Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dolphone

Calls and texts for the Flipper One. Fit a cellular modem in the M.2 slot and your Flipper One rings: calls, texts, emergency alerts, contacts and a call log, with Bluetooth headsets, the 3.5 mm jack and speakerphone. On the panel it is a set of ordinary FlipCTL apps driven by the Flipper’s own buttons; in desktop mode it is a proper desktop app; from a shell it is phonectl.

An incoming call from Alex Example on the Flipper One's panel The call in progress, on the speaker

The keypad matching 567 to Jordan Doe A conversation with Alex Example A full-screen emergency alert

Unofficial. Dolphone is not made or endorsed by Flipper Devices. Not for emergencies. It passes 000/112/911 to the modem, which the network treats as emergency calls, and never blocks them, but it is hobby software on a pre-release device: carry a real phone.

What it does

CallsDial, answer, decline, hold, swap and merge (conference), call waiting, keypad tones, mute; speed dial (hold 2–9) and voicemail (hold 1); T9 name matching; the country’s own busy and ringback tones; a call log with missed calls
Calls over the internetA SIP account (through baresip) for when there is no signal, or always, receiving texts over SIP too; same screens, buttons and headsets; a SIP call and a mobile call can be held and swapped. Emergency calls always go over the mobile network
TextsConversations, multipart texts with a live segment counter, quick replies, delivery reports, resend for a failed text, search, export to Android (SMS Backup & Restore)
Picture messages and group textsMMS through mmsd-tng: pictures shown in the desktop app, group conversations as one thread, replies to everyone
Carrier servicesCall forwarding (**21*n#, *#21#, ##002#), caller ID (#31#), call waiting, and USSD balance codes from the keypad; a check of the modem’s own setup for calls (USB audio, VoLTE, the carrier profile) that sets it right with one press
Emergency alertsCell broadcast (national, extreme, severe, Amber, test), full screen over everything, sounding through silent mode
AudioBuilt-in speaker (with echo cancellation), 3.5 mm headsets and speakers, Bluetooth headsets (HFP) and speakers (A2DP), an earpiece where the machine has one; calls follow a headset in and out; music pauses for a call; the LEDs flash for a ringing call if you like
Contacts.vcf files; copy your phonebook from your current phone over Bluetooth (no account, no sign-in), or import an export from Google or iCloud; names and numbers only
The panelFlipCTL apps under Apps > Phone that use every button the way FlipCTL means it: Run answers, Escape declines, PTT mutes (and held, switches the ringer), the touchpad taps the keypad, the motor has a vibration for each kind of news
Desktop modeA full desktop app with keyboard shortcuts, notifications with Answer / Decline / Reply, a tray icon, tel: and sms: links, and Copy for one-time codes
Your laptopKDE Connect (GSConnect on GNOME): texts and call alerts on a laptop on the same network, and replies from it, paired on the device
Scriptsphonectl (with --json), a D-Bus API, standing grants (“this program may text these numbers, 4 an hour”), rules that run a command when a matching text arrives, an optional local SMS gateway (HTTP), texts when the uplink goes down, and an audit log of what programs asked for
SafetyPremium numbers and short codes always need a press on the device; outside programs wait for one to dial, text, or change contacts, and cannot read messages, answer, or change settings unless you allow it; rate limits; blocked numbers; silence for unknown callers; a warning when the network forces 2G, and a switch to refuse it; the message store can be encrypted, its key sealed to the machine
The phone’s stateSIM PIN on the keypad; a Today screen of missed calls and unread texts; stays awake for calls and catches up after sleep; follows the modem through resets; records which SIM each text and call went through

How it fits together

 FlipCTL (unchanged)        Dolphone apps on the panel         dolphone-desktop    phonectl
        │                    Phone · Messages · Settings        (desktop mode)     (scripts)
        │ flipctl open                 │                              │                │
        └──────────────┐               │      D-Bus (session)        │                │
                       ▼               ▼                              ▼                ▼
                  ┌──────────────────────────────────────────────────────────────────────┐
                  │ phoned: calls, texts, alerts, call audio, contacts, Bluetooth, the store │
                  └───────┬───────────────┬────────────────┬──────────────┬─────────────┘
                          │ D-Bus (system)│ PipeWire       │ BlueZ        │ baresip
                     ModemManager      speaker / jack /   headsets,      SIP calls
                          │            Bluetooth / modem  phonebook
                     M.2 modem (RM520N-GL)

phoned is the only thing that talks to ModemManager’s voice and messaging and the only thing that routes call audio, so the panel, the desktop and your scripts never disagree about a call. More in docs/ARCHITECTURE.md.

Status

Everything above works against fake-mm, a stand-in ModemManager that mmcli itself accepts, and a second baresip playing the far end of SIP calls. The tests:

  • 224 end-to-end checks (scripts/e2e.sh): phoned, the fake modem, a fake mmsd-tng and phonectl playing through calls, texts, MMS, alerts, SIM PIN and SIM codes, an emergency call with the SIM locked, SIP, service codes, the modem’s setup, grants, rules, contacts, the encrypted store, serial call audio through real PipeWire, and what an untrusted script is and is not allowed to do; they also pass with every CPU core busy, as a stand-in for a slow device;
  • 5 live UI tests (scripts/ui-e2e.sh): the Dialer, Messages, Settings and the desktop app (and its tray icon), driven through their real input against a live phoned, with a screenshot of each step;
  • 10 KDE Connect checks (scripts/kdeconnect-e2e.sh) against the real kdeconnectd: pairing, texts both ways, a ringing call;
  • 184 unit tests, including every panel screen rendered and checked against the panel’s fonts.

Before release it had thirty reviews: ten for features and fit, then twenty more on security from a same-user program and from the network, Rust robustness, concurrency, data integrity, packaging, the modem, audio, emergency calling, telephony standards, numbers abroad, accessibility, the scripting API, the tests themselves, onboarding, FlipCTL’s conventions, licensing, battery use, failure recovery and privacy. What they found is fixed or written down in ROADMAP.md.

The panel apps also run in the real FlipCTL (its browser view, in a VM). Dolphone has not yet carried a real call: that needs the hardware test in docs/HARDWARE.md, whose deciding question is whether the RM520N-GL’s USB audio gives Linux two-way call audio.

Install on the Flipper One

Flipper OS is Debian 13 (arm64):

sudo apt install ./dolphone_0.1.0_arm64.deb   # built with scripts/package-deb.sh until the first release
dolphone-install-apps                          # as yourself: Apps > Phone on the panel

The install asks one question: whether Dolphone may send setup commands to the modem, which turns on ModemManager’s debug mode (yes by default on a Flipper One, where call audio needs it; sudo dolphone-modem-access on|off changes it later, and port on gives Dolphone a spare AT port instead, untested on a real modem). The package recommends what the optional parts use: mmsd-tng for picture messages and group texts, baresip for SIP, bluez-obexd for copying contacts from a phone, wl-clipboard or xclip for Copy, and suggests espeak-ng for spoken caller ID. More in docs/install.md.

Other devices

Dolphone is built for the Flipper One, and its panel apps are FlipCTL’s. Underneath, phoned only needs Linux, ModemManager, PipeWire and BlueZ, so the daemon, phonectl and the desktop app run on other machines with a modem too. What differs between machines (mixer controls, where the modem’s audio is, which front ends fit) is a device profile: one TOML file, picked automatically and shown by phonectl device. These ship:

DeviceStatusWhat works
Flipper Oneexpectedeverything: the panel apps, desktop mode, the CLI
ClockworkPi uConsole (4G extension)untestedcalls (the SIM7600’s serial call audio bridged to PipeWire), texts, SIP
Raspberry Pi with a USB modem or HATuntestedtexts, SIP calls; mobile calls where the modem offers USB audio
PinePhoneuntestedcalls through its UCM voice-call profile, on the earpiece first; texts, SIP
Purism Librem 5untestedcalls (its modem is a separate sound card, bridged as on the Flipper), on the earpiece first; texts, SIP
Laptopuntestedthe desktop app, SIP calls, texts and calls where the WWAN modem allows them
Anything elseexpectedthe generic profile: default devices, nothing touched

Each profile says where its values came from; “untested” means nobody has run docs/HARDWARE.md on one yet. Adding a machine is one file, no code: docs/DEVICES.md. There is an amd64 package for laptops.

Try it without a Flipper One

On an Apple Silicon Mac with OrbStack:

scripts/dev-setup.sh                 # a Debian 13 arm64 machine, FlipCTL built, fake modem in
orb -m f1 scripts/run-dev.sh         # build, install, start; open http://f1.orb.local:8899/
orb -m f1 ~/.local/bin/phonectl fake ring 0491570156     # and watch the panel
orb -m f1 ~/.local/bin/phonectl fake sms 0491570156 "hello"
orb -m f1 ~/.local/bin/phonectl fake broadcast 4370 "Test alert"

More in docs/DEVELOPING.md.

From a shell

phonectl send --wait 0491570156 "build finished"   # waits for a press on the device, then for the network
phonectl mms 0491570156 --file photo.jpg "Look"    # a picture message (needs mmsd-tng)
phonectl modem setup                               # is the modem set up for calls? (apply to fix it)
phonectl otp --from 0491570156 --wait 60           # prints the code from the next text (needs a read grant)
phonectl grant 0491570156 4 30                      # ask to text it without asking, 4 an hour, for 30 days
phonectl --json watch                               # every event, one JSON object per line
phonectl contacts import ~/Downloads/contacts.vcf
phonectl device                                     # which device profile, and why

phonectl help lists the rest. For scripts, it exits 0 when done (or waiting for the device), 1 when tried and failed, 2 when not allowed, 3 for a bad argument or something this phone does not do, 4 over a rate limit, and 5 when phoned is not running. Everything it does is on the D-Bus API, which busctl and gdbus reach as well.

What is not done

  • A real call. Everything is tested against stand-ins; the hardware test in docs/HARDWARE.md comes first, and with it the Quectel commands that only real firmware can confirm.
  • A large-text mode, fonts beyond Latin and Cyrillic, and news outside Dolphone’s apps on the panel (a status-bar badge, a toast): FlipCTL has none of these, and Dolphone may add them later as options. Until then, names the font cannot draw fall back to the number.
  • Sending texts to a SIP address: baresip 1.1.0 cannot; texts over SIP are received, and replies go over the mobile network.
  • RCS: there is no route to it from Linux.

ROADMAP.md has the details.

Docs

License

Source: MIT. The panel apps and the desktop app link Slint (the panel apps through flipctl-app) under GPL-3.0-only, so those binaries are GPL-3.0-only; each release carries the full corresponding source, and the Debian package says where it is in /usr/share/doc/dolphone/copyright. phoned, phonectl and the gateway are MIT, with their dependencies’ notices in THIRD-PARTY-LICENSES.md (SQLCipher’s among them). The panel fonts are CC BY-SA 3.0, Unlicense and MIT, and the AppImage runtime at the head of each bundle has its own notices; see each bundle’s NOTICE.md.

Install

On the Flipper One

Dolphone installs on Flipper OS (Debian 13, arm64) from the Debian package attached to each release (until the first one, build it with scripts/package-deb.sh on the device or any arm64 Debian 13 machine):

sudo apt install ./dolphone_0.1.0_arm64.deb
dolphone-install-apps            # as yourself: Apps > Phone on the panel
phonectl device                  # should say Flipper One

The package brings:

phonedthe phone daemon, started in every user session (systemd user unit, D-Bus activated)
phonectlthe command line, for scripts
dolphone-desktopthe desktop-mode app, with tel: and sms: links
dolphone-install-appscopies the panel apps (Phone, Messages, Phone Settings) into ~/Apps/Phone
device profiles/usr/share/dolphone/devices/: how each machine’s audio is wired (Devices)
PipeWire and WirePlumber configan echo canceller for speakerphone; Bluetooth hands-free
udev ruleslet the modem wake the board for a call or a text, the dolphone group flash the Flipper One’s LEDs, and (when chosen) give that group a spare AT port of the modem’s
dolphone-modem-accessturns ModemManager’s debug mode on or off, or gives Dolphone a spare AT port instead (see below)
a polkit rulelets your active local session use the modem’s calls, texts, USSD and device control (which, in debug mode, includes AT commands)

It needs ModemManager, PipeWire and WirePlumber (pulled in by apt), and conflicts with GNOME Calls and Chatty, which would race it for every call and text. It recommends, and apt installs unless told not to:

PackageFor
baresip-corecalls over the internet (SIP)
bluez, bluez-obexdBluetooth headsets, and copying contacts from your phone
mmsd-tngpicture messages and group texts (MMS)
pipewire-pulseSIP call audio (baresip plays through PipeWire’s Pulse server)
alsa-utilsthe mixer commands device profiles run
wl-clipboard or xclipCopy in the desktop app
espeak-ng (suggested)saying who is calling

The one question

The install asks: Let Dolphone send setup commands to the modem? Dolphone sets the modem up for calls, tells it where call audio goes each time it starts, and runs call forwarding and caller-ID codes, all as AT commands, which ModemManager passes on only in its debug mode. Debug mode also makes ModemManager’s log more detailed, and with an AT modem that log can hold numbers and texts. It also lets any program running as you send the modem AT commands through ModemManager (mmcli --command), around Dolphone’s confirmations. On a Flipper One call audio needs it, so yes is the default there; elsewhere it is no. Change it any time:

sudo dolphone-modem-access on        # or off, or status
sudo dpkg-reconfigure dolphone       # ask again

On a Quectel RM520N-GL there is another way, not yet tried on a real modem: a spare AT port that ModemManager is told to leave alone, which phoned uses itself, so debug mode can stay off. It is not part of the question, since it depends on the modem:

sudo dolphone-modem-access port on   # or port off
sudo dolphone-modem-access off       # debug mode, if it was on
sudo adduser $USER dolphone          # the port's group; then log in again
phonectl modem setup                 # says which way the commands go

Devices has the details, and how to do it for another modem.

Phone Settings > Modem shows Turn on when it is off, with a password prompt.

After installing

  • Contacts: in Phone Settings > Bluetooth, pair your current phone and press View on it to copy its contacts (allow it on the phone); or phonectl contacts import contacts.vcf with an export from contacts.google.com or icloud.com; or drop .vcf files into ~/.local/share/dolphone/contacts/. Only names and numbers are kept.
  • Voicemail: Phone Settings > Calls > Voicemail number (some carriers’ numbers are known and filled in from the SIM).
  • Calls over the internet: Phone Settings > Calls over the internet, with your SIP provider’s address, password and (if they give one) proxy.
  • Board audio: if the speaker or the jack stays silent, the profile’s mixer controls do not match your board; see Devices and Audio.
  • Modem setup: Phone Settings > Modem says whether the modem is set up for calls (USB audio, VoLTE, the carrier profile); Set up makes the changes and restarts it. phonectl modem setup is the same from a shell.
  • Picture messages: with mmsd-tng installed, MMS just arrives. If it does not, the carrier’s MMS settings (MMSC, proxy, APN) are in ~/.mms/modemmanager/mms (stop mmsd-tng before editing it).
  • LEDs for a ringing call: sudo adduser $USER dolphone and log in again; they flash while a call rings (the flash_on_ring setting, on by default).
  • An encrypted message store: systemctl --user stop phoned, then phoned --encrypt-store, then systemctl --user start phoned. The key is sealed by systemd to you on this machine; phoned --decrypt-store undoes it. See Security for what it does and does not protect.
  • Scripts that read messages (an OTP reader, say) need a read grant: phonectl set read_grants phonectl, allowed on the device. Grants go by program name, so a grant to phonectl is a grant to every script that uses it. Scripts that text without asking each time need a standing grant: phonectl grant <numbers> <per-hour> <days>, allowed once on the device.

On other machines

The same package (arm64), or the amd64 one for a laptop, installs on any Debian 13 machine; the panel apps are only in the arm64 package and only useful under FlipCTL. phonectl device says which profile phoned picked; Devices has what works where and how to add a machine.

Without any hardware

The whole stack runs against fake-mm, a scriptable stand-in modem, on an Apple Silicon Mac with OrbStack or any arm64 Debian 13 machine. See Developing.

Buttons

Dolphone keeps FlipCTL’s roles for the five soft keys everywhere: Escape backs out or cancels, View switches view or shows information, Edit edits, Run does the main thing. The middle soft key, silkscreened power, does reach the app in front (flipctl forwards it like the other four), but Dolphone leaves its slot empty on purpose: a press meant for the phone’s power is never taken as Answer, End or Allow. Back and the app switcher never reach an app: flipctl keeps them. Back leaves the app (on a call, the call carries on; come back to it with the switcher), and the switcher lists running apps, a live call among them.

A hold is 0.6 s or more by default (the long_press_ms setting, 300–3000 ms).

The touchpad is read by the app that has the keyboard, the one in front; an app in the background ignores it.

Dialer

ScreenD-pad / OkEscapeViewEditRunPTTTouchpad
Keypadmove, press a key; hold 0 for +; on an empty keypad hold 1 for voicemail, hold 2–9 for speed dialclear, then exitRecentsdelete (hold: clear)Call (empty: redial; a name matched by its letters, “567” for Jordan: call them)hold: Ring → Vibrate → Silenttap a key, swipe to move
Keypad, SIM lockedtype the PIN (shown as *)clearRecentsdeleteUnlock (an emergency number still dials)tap a key
RecentsmovebackContactstextcall backswipe
Contactsmove, Left/Right jump a letter; Ok calls, hold Ok to give the contact a speed-dial keybackKeypadtextcallswipe
Speed dial pickerchoose 2–9backclear that keyset
Incoming callDecline (hold, with “Hold to decline” on)number detailsreply with a textAnswersilence the ringer
Reply pickerchoosebacksend and decline
In callOk swaps two calls; hold Ok merges themkeypadaudio routeEnd (ignored for 1.5 s after answering)mute / unmute
In-call keypadmove, Ok sends the toneclosecloseaudio routeEndtap a key
Emergency alertOk dismissesdismissDismiss (Next while more wait)
ConfirmationDenyAllow
Service code replyclosereply on the keypad

Emergency alerts not yet dismissed wait in turn rather than replacing each other: the most severe first (National, Extreme, Severe, Amber, Test), alerts of one level in the order they came, with “1 of 3” in the breadcrumb while more wait. A worse alert arriving takes the screen at once; the one it covered comes back after it.

Keys are ignored for 400 ms after a call, an alert or a confirmation appears unasked, so a press meant for whatever was on screen cannot answer a call or allow a request. A press ignored like this, End in the 1.5 s after answering, and a short press of Decline when it needs a hold each give the “refused” stutter, so a press that did nothing does not feel like one that did.

The touchpad works as a keypad once it has learnt its own size from a few strokes; until then it moves the selection like the D-pad.

Vibrations

Each kind of news has its own feel, so it can be told in a pocket. Messages uses the same feel for a text. Everything but a key, a thump, a refused press and an emergency alert stays still when the ringer is on Silent.

a keya tick
a call connects or endsa thump
a texttwo short
a missed callthree short
a second call waitingtwo long
a call dropped (not ended here)one long
a text not sentshort, short, long
an emergency alertthe alert tone’s rhythm (long, short, short, twice), even on silent
a press ignored on purposea quick stutter (three very short)

Messages

ScreenD-pad / OkEscapeViewEditRun
Threadsmove, Ok opensexitnew messageopen
ConversationUp/Down scroll, Left/Right page, Ok writesbackoptionswritecall
Options (Resend failed text, Quick reply, Attach a picture, Call, Block, Delete)movebackbackselect
Quick replychoosebackedit it firstuse (to the preview)
Attach a picturechoose, Ok as RunbackAdd (to the preview), or Remove one already added
Keyboard (FlipCTL’s)typecancel123backspacedone
Previewedit againAttach (Pictures once one is added)edit againSend
Locked (a Messages PIN is set)Ok types the PINexittype the PINUnlock: type the PIN
PIN keyboard (opens on 123, the PIN shown as *)typeexit MessagesABCbackspaceUnlock (4 to 12 digits)

With a Messages PIN set (Settings > Privacy), Messages opens on the PIN and shows nothing of the texts until it is right; it stays open for the time set there, then asks again. A wrong PIN says so; after five, phoned makes you wait. A number the Dialer hands over while locked opens once the PIN is in.

Attach a picture lists the newest 50 pictures (JPEG, PNG, GIF, WebP) in the Pictures and Downloads folders (XDG’s, from user-dirs.dirs, else ~/Pictures and ~/Downloads) and the folders directly in them, with each one’s size and date. Links are not followed, and a file phoned would not send (empty, unreadable, over 8 MB) is left out. What is added is named above the text on the preview, which is then a picture message (a text is optional); to take one off, open the pictures again and Remove it. Up to 10, 8 MB in all, go in one message; a group’s goes to everyone. Without pictures the preview sends a text, as before.

A picture message shows its text with [Picture] (or [Audio], [Contact]) below: the panel is too small for the picture, which the desktop app shows. A group conversation is one thread, named after everyone on it; a reply goes to all of them.

Phone Settings

Left / RightOk / RunEditViewEscape
ON/OFF rowstoggletoggleup a page
< value > rowsstepstepup a page
Pages (Call audio, Bluetooth, Laptops, Calls, Calls over the internet, Texts, Emergency alerts, Privacy, Modem, About)Right opensopenup a page
Typed rows (voicemail number, SIP account, password, proxy)open the keyboardopen the keyboardup a page
Bluetooth devicepair / connect / disconnectforgetcopy contacts (a paired phone)
Blocked numberunblockunblock
A program allowed to textrevokerevoke
Privacy > Messages PINRight setsSet / Change: the new PIN on the keyboard, then againRemove (asks first)up a page
Privacy > Messages stay openstep 1, 5, 15 minstepup a page
Modem pageSet up when the modem’s setup has changes to make, or Turn on when ModemManager’s debug mode is off; both ask firstup a page

Desktop app

Keys
Ctrl+1 … Ctrl+4Calls, Messages, Contacts, Settings
Ctrl+Na new message
Ctrl+Enteranswer the ringing call
Ctrl+Backspacedecline the ringing call, or end the live one
Ctrl+Mmute or unmute
Tab, Shift+Tabthe next or last control; over a call, a confirmation or an alert, round that card’s own buttons only
Up, Downalong a list (calls, conversations, contacts, the lists in Settings)
Entera list row’s main thing (call, open, unblock…), or press the control with the focus
Spacechoose a list row, or press the control with the focus

The call card opens with the focus on Answer (on Keypad once the call is live, away from End); a confirmation on Deny; an alert on Dismiss (Next alert while more wait, with “1 of 3” beside the time; the most severe comes first).

With a Messages PIN set, the Messages page is a PIN field and Unlock (Enter unlocks too) until it is right; once open, Lock beside New closes it again. Settings > Messages PIN sets or changes it (the new PIN twice), removes it, and picks how long Messages stays open (1, 5 or 15 minutes).

In a conversation, Attach opens the desktop’s file chooser (through the XDG desktop portal) for pictures; what is picked is named above the text, Remove drops it, and Send sends the lot as one picture message. Without a portal, Attach says so.

Rules on incoming texts

A rule runs a command when a matching text arrives: a text that says STATUS gets the uplink’s state back, a text with a code reboots a router, a one-time code is handed to a script. Rules live in ~/.config/dolphone/rules.json, a list:

[
  { "from": "+61491570156", "contains": "STATUS", "secret": "4821",
    "run": ["/usr/local/bin/wan-status"], "reply": true },
  { "from": "*", "contains": "Your code is", "run": ["/home/me/bin/otp-to-clipboard"] }
]
Field
fromrequiredthe sender, in any form (0491 570 156, +61491570156), or * for anyone
containsoptionaltext the message must contain, ignoring case
secretoptionaltext it must also contain, exactly: a shared password for rules that do something
runrequiredthe program and its arguments; run directly, never through a shell
replyoptionaltrue to text back what the program prints

A field phoned does not know makes the whole file invalid (it says so in its log) rather than being ignored: a misspelled "contains" would otherwise leave a rule that fires on every text from that sender. phoned reads the file again for every text, so changes count at once.

What the program gets

The text as JSON on its standard input:

{"from": "+61491570156", "body": "STATUS 4821", "at": 1793228520}

at is when it was sent, in Unix seconds. The program has 30 seconds; after that it is stopped. Every rule that matches runs, each on its own, at the same time.

With "reply": true, what the program prints (up to 600 characters) is texted back to the sender, but only to a sender the rule names: a rule for * never replies, so a rule cannot be made to text anyone who writes to it. A program that exits with an error sends no reply, and what it printed to its standard error goes to phoned’s log (journalctl --user -u phoned), for you to see why.

What never runs a rule

  • Texts that arrive over SIP: a SIP sender is not authenticated the way a mobile network’s is, and a reply goes out as a text on your account.
  • Texts from blocked numbers, and flash texts, which are shown and not kept.

An example: STATUS

#!/bin/sh
# /usr/local/bin/wan-status: say whether the uplink is up.
ip -brief link show eth0 | awk '{print "eth0 is " tolower($2)}'

With the first rule above, texting STATUS 4821 from +61491570156 gets eth0 is up back. For texting when the uplink goes down without being asked, see the uplink_alert_to setting instead.

Architecture

The pieces

CrateRuns asJob
phone-corelibrarynumbers (canonical form, display, emergency / premium / short classes, prefixes), message text cleaning, SMS segment counting and one-time codes, vCard contacts (import, merge, edit), supplementary-service codes (TS 22.030), the store (SQLite, or SQLCipher when encrypted: messages with their MMS parts, calls, alerts, grants, audit; behind the db feature, which the panel apps leave out), the countries’ call-progress tones, local time
phone-apilibrarythe D-Bus names, wire types and client proxies; a script author can use it alone
phone-buslibrarya front end’s worker thread: subscribes to everything phoned says, runs calls off the UI thread, survives phoned restarting
phonedsystemd user servicethe phone: ModemManager client, call state, texts, MMS through mmsd-tng, emergency alerts, confirmations and grants, call audio (including a serial PCM bridge), ringer, tones and LEDs, Bluetooth and the phonebook copy, jack, SIP through baresip, KDE Connect, device profiles, the modem’s AT setup and service codes, rules on texts, uplink alerts, sleep and wake, desktop notifications, the store’s sealed key
dolphone-gatewayoptional user servicean HTTP SMS gateway on phoned’s API, localhost only by default
phonectlCLIeverything a front end can do, for scripts; phonectl fake ... plays the far end against fake-mm
fake-mmdev servicea ModemManager stand-in with one scriptable modem: calls, texts, delivery reports, cell broadcast, a SIM PIN, call waiting, conferences, AT commands as a Quectel answers them (refused without debug mode); and fake-mmsd, an mmsd-tng stand-in on the session bus
apps/dialer, apps/messages, apps/settingsFlipCTL appsthe panel front ends (Wayland clients on flipctl’s compositor, drawn with flipctl-app’s widgets)
apps/desktopdesktop appthe desktop-mode front end

Rules the design keeps

  1. Add to FlipCTL, never replace it. No launcher, status bar or keyboard of our own. The apps use FlipCTL’s widgets, fonts and soft-key roles; phoned raises them with flipctl open; SIM, APN and data settings stay in Flipper’s own 5G Modem app. What Dolphone sets on the modem is only what calls need: VoLTE, the carrier profile, USB call audio, and refusing 2G when asked.
  2. One owner per thing. phoned is ModemManager’s only client for Voice and Messaging (NetworkManager and Flipper’s 5G Modem app read the same modem for data and signal, untouched; mmsd-tng takes the MMS notices from Messaging and fetches the messages for phoned), with one deliberate exception: PipeWire’s Bluetooth hands-free support follows ModemManager’s calls so a headset rings and its button answers, which is the only way to get that without reimplementing HFP. phoned is the only thing that routes call audio. Front ends never talk to either. The package conflicts with GNOME Calls and Chatty, which would race phoned for every call and text.
  3. Nothing waits on a UI thread, and nothing slow waits on phoned’s async threads. Bus calls run in phone-bus’s worker; PipeWire’s tools run on blocking threads with time limits. (A synchronous pw-dump once froze call signals for 25 s.)
  4. Untrusted input is cleaned once, on the way in. Sender names and bodies lose control characters, bidi overrides and zero-width characters, and are capped, before they reach the store, a log or a screen.
  5. The Flipper One first; other machines by data. Everything about the hardware around the modem is a device profile (dist/devices/*.toml, see DEVICES.md), so a new machine is a file, not a branch in the code.
  6. Measured on the panel, in the fonts it draws. Every position on the panel is computed in Rust, text is wrapped and cut with the bitmap font that will draw it, and the screens are rendered headlessly in tests (DOLPHONE_RENDER=1 cargo test).

A call, end to end

  1. The network rings. ModemManager adds a Call object and emits Voice.CallAdded.
  2. phoned reads the number, checks the block list, starts tracking the call and watching its StateChanged, emits Calls.CallAdded, starts the ringer (PipeWire, notification role), runs flipctl open <Dialer bundle> and posts a desktop notification if a desktop is running.
  3. The Dialer comes up on the panel (FlipCTL starts it or brings it forward) and shows the incoming screen, ignoring keys for 400 ms so a press meant for the app underneath cannot answer. It vibrates the motor while the call rings.
  4. Run answers: Dialer → Calls.Answer → phoned → Call.Accept (or Voice.HoldAndAccept when another call is live).
  5. ModemManager reports ACTIVE; phoned stops the ringer, starts the two PipeWire loopbacks for the chosen route (modem ↔ speaker with echo cancellation, jack, or Bluetooth), and emits CallChanged. Every front end shows the call.
  6. End (Run, after a 1.5 s lockout), the far end hanging up, or the headset button: TERMINATED. phoned logs the call (answered, missed, declined, cancelled, blocked), tears the audio down, emits CallRemoved, posts a missed-call notification if it was one, and has ModemManager delete the Call object.

A SIP call

phoned runs its own baresip (config and account in ~/.config/dolphone/sip/, 0600) and drives it over baresip’s D-Bus control module, listening only to the connection whose process it started. baresip’s events (CALL_INCOMING, CALL_ESTABLISHED, CALL_CLOSED …) move a SIP call through the same states as a cellular one, so it is the same Live call to every front end, with internet: true. Audio is baresip’s own, through PipeWire: phoned points it at the route’s output and microphone with auplay/ausrc as the route changes. Which network a call takes is decided in Phone::dial: a SIP address always SIP, an emergency number never, and otherwise the owner’s choice (always, or only with no mobile signal).

A text, end to end

Incoming: Messaging.Added → phoned waits for a multipart message to finish arriving, cleans sender and body, drops it if blocked, stores it, emits MessageAdded and notifies, then deletes it from the modem’s storage (a full modem stops accepting texts). Then the owner’s rules (~/.config/dolphone/rules.json) run for a matching text, with the text as JSON on stdin and no shell. Outgoing: Messages.Send → confirmation rules (or a standing grant) → stored as Sending → Messaging.Create + Sms.Send → Sent or Failed → with delivery reports on, the modem’s copy is kept until the network reports Delivered, then deleted.

An emergency alert

phoned tells the modem’s CellBroadcast interface which channels to listen to (the 3GPP alert ranges). Each Cbm is classed by its channel (national, extreme, severe, Amber, test, information), dropped if the owner turned that class off (national never can be), stored once however often it is rebroadcast, announced as Messages.AlertReceived, sounded with the alert tone whatever the ringer says, put full screen on the panel and the desktop, and deleted from the modem.

Confirmations

Dialling or texting from an outside program (a script, phonectl, another app), any premium number or short code from anyone, an emergency number from an outside program, and wiping the store all wait as a Request. phoned shows it on the panel (by raising the Dialer) and the desktop; only a press there, or a click in the desktop session, lets it go. Requests expire after two minutes. Who is “inside” is decided by the executable behind the D-Bus caller; see SECURITY.md for what that does and does not protect.

A standing grant lets one program text listed numbers (or a prefix) without asking each time, up to its own hourly limit and until a date; it is asked for with Settings.RequestGrant, allowed once on the device, and revoked in Phone Settings. Premium, short and emergency numbers can never be granted. Every outside request is written to the audit log (numbers masked), shown in Phone Settings.

Where things live

WhatWhere
Messages, call log, alerts, grants, audit, blocks, settings~/.local/share/dolphone/dolphone.db (0600)
MMS pictures and other parts~/.local/share/dolphone/mms/ (0700)
KDE Connect: this device’s certificate, paired laptops~/.local/share/dolphone/kdeconnect/
Contacts~/.local/share/dolphone/contacts/*.vcf
The store’s sealed key (when encrypted)~/.config/dolphone/store-key.cred
The SMS gateway’s token~/.config/dolphone/gateway.token (0600)
Device profilesbuilt in; /usr/share/dolphone/devices/; ~/.config/dolphone/devices/
The owner’s audio overrides~/.config/dolphone/audio.json
Rules on incoming texts~/.config/dolphone/rules.json
SIP: baresip’s config, account and password~/.config/dolphone/sip/ (0700; accounts and password 0600)
Ring tones, handoff file, desktop socket$XDG_RUNTIME_DIR/dolphone/
Panel apps~/Apps/Phone/dolphone-*-aarch64.fap.AppImage

Audio

phoned owns call audio. FlipCTL pins a hosted app’s sound to the panel speaker and keeps it away from the sound card, so no app could route a call even if it wanted to.

Routes

RouteOutputMicrophoneEcho cancelled
Speakerthe codec, speaker side (dolphone_ec_sink)the device mic (dolphone_ec_source)yes, WebRTC in PipeWire
Headset (3.5 mm)the codec, headphone sidethe headset’s mic (TRRS)no
Bluetooththe headset’s sink, switched to its hands-free profile for the callthe headset’s micthe headset’s own
Earpiece (phones that have one)the codec’s earpiece, switched by the profile’s earpiece_commandsthe device micno

A mobile call is two pw-loopbacks phoned starts when the call connects:

dolphone-downlink:  modem capture  ─▶ output for the route
dolphone-uplink:    mic for route  ─▶ modem playback

Mute stops the uplink only. Changing route mid-call restarts only the loopback whose end moved (speaker to headset on the codec moves neither: only the mixer commands run), so the other side of the call is not cut. They stop when the last call ends. An outgoing call’s audio starts while it rings out, for the network’s own ringback and announcements.

Starting and stopping are asked for without waiting and carried out in order on one audio thread, so a call that ends quickly cannot have its stop run before its start. Each loopback’s streams carry node.dont-reconnect and node.dont-fallback, so when their device goes WirePlumber ends them rather than moving them to the default device (wireplumber’s linking/prepare-link.lua); during a call phoned checks every couple of seconds and starts again any loopback that has died, which also brings a call’s audio back after PipeWire restarts.

On the speaker the call goes through the echo canceller (dist/pipewire/dolphone-echo-cancel.conf). Its own streams name no device, so when a speaker call starts phoned points them at the profile’s codec_sink and mic_source (target.object in PipeWire’s default metadata, which WirePlumber honours: linking/find-defined-target.lua); otherwise they would follow the default sink and source.

On Bluetooth a headset is usually in A2DP, which has no microphone. When a call starts there phoned switches it to the highest-priority profile with an input route (headset-head-unit, HFP/HSP, as WirePlumber’s own device/autoswitch-bluetooth-profile.lua picks), with pw-cli set-param <device> Profile '{ index: N, save: false }' so the choice is not remembered, waits for the headset’s microphone to appear, and starts the loopbacks. The old profile comes back when the call ends or leaves Bluetooth.

A SIP call’s audio is baresip’s: it plays and records through PipeWire itself, and phoned points it at the route’s output and microphone (auplay, ausrc) when the call connects and whenever the route changes. Mute is baresip’s mute.

What switches routes

  • Edit in a call cycles Earpiece → Speaker → Headset → Bluetooth, skipping what is not there; a call starts on the earpiece where the profile has one, otherwise on the speaker (and the desktop app’s route menu sets one).
  • A headset plugged into the jack takes the call; unplugging it falls back to the speaker. phoned reads SW_HEADPHONE_INSERT from whichever input device reports it, and notices a device that goes away and comes back. It watches /dev/input with inotify rather than rescanning it on a timer, so nothing wakes while nothing changes: a new event* node (or udev giving it its permissions) brings a rescan at once and another a second later. Underneath that is a rescan once a minute, for when /dev/input cannot be watched, and one whenever a watched device goes.
  • A Bluetooth headset connecting takes the call; it disconnecting falls back to the jack or the speaker.
  • A route the user picked by hand stays until the call ends, except that a device disappearing always falls back.

The machine’s own wiring: device profiles

Which mixer controls switch speaker and headphones, which sink and source to use, how to spot the modem’s audio card, and anything to run as a call starts and ends are the device profile’s (DEVICES.md). On the Flipper One that is the NAU8822’s Speaker Playback Switch and Headphone Playback Switch, switched with amixer, because the driver has no UCM profile; its jack and headset button are watched by the RP2350, which passes them on. A phone whose modem is wired to the codec (the PinePhone) needs no loopbacks, only its UCM Voice Call verb, which its profile runs with call_start_commands.

~/.config/dolphone/audio.json (example in dist/audio.example.json) overrides any key of the profile, for the machine in front of you.

Some modems send call audio over a serial port instead of a sound card: the SIMCom SIM7600 in the uConsole’s 4G extension sends mono 16-bit PCM on its audio port once AT+CPCMREG=1 is sent (SIMCom’s USB AUDIO Application Note V1.03, §2). ModemManager’s SIMCom plugin does that itself (plugins/simtech/mm-shared-simtech.c): it sends AT+CPCMREG=1 and =0 as a call’s audio starts and ends, names the port as the call’s AudioPort, and reports the rate in its AudioFormat, 8 kHz or 16 kHz where the firmware has AT+CPCMFRM=1. So the profile sends nothing, and needs no ModemManager debug mode. phoned uses the call’s AudioPort, or else the profile’s serial_pcm, and bridges it with two pw-cat processes at that rate (8 kHz when none is reported), one each way, as nodes dolphone-downlink and dolphone-uplink. The port is flushed each time a leg starts, so stale bytes never play as noise. The port needs the dialout group.

The modem’s PCM clock and the sound card’s are different clocks, so over a long call one runs ahead of the other. With pw-cat on the port directly, latency would grow without bound in the port’s buffer, or the stream would run dry. So phoned’s own thread sits between the port and both pw-cats:

modem port ─▶ [serial-pcm thread] ─▶ pipe ─▶ pw-cat --playback   (downlink)
modem port ◀─ [serial-pcm thread] ◀─ buffer ◀─ pw-cat --record   (uplink)

It reads the port 20 ms frame by frame. Each frame goes into the downlink’s pipe, and for each one, one frame of the uplink goes back to the modem. The uplink so runs at the modem’s own clock, and its buffer in phoned is the only place the drift collects. The downlink’s buffer is the pipe itself, whose fill the kernel reports (FIONREAD), so it counts what pw-cat has really taken. Each buffer may wander between 20 ms and 200 ms. Past either bound it is brought back to 60 ms, a whole frame at a time: frames are dropped, or silence is added. A buffer whose low point has stayed over 60 ms for a second is trimmed back to it too, so latency picked up once (a stream’s start, a stall) does not last the call. At a realistic drift that is a 20 ms skip or gap every few minutes. If the modem sends nothing for 200 ms, the uplink carries on at phoned’s own clock. While muted, the modem gets silence.

pw-cat‘s raw mode goes through stdio, which moves 4096 bytes at a time: 256 ms at 8 kHz, read ahead of playing and written in bursts. phoned runs both legs under coreutils’ stdbuf -i0 -o0 (it then runs pw-cat itself, so ps shows pw-cat), which makes pw-cat move 5 to 20 ms at a time. Both legs also ask for --latency 20ms. Without stdbuf the bridge allows for the bursts: it estimates what pw-cat has read but not yet played, and raises the uplink’s drop bound by the largest burst seen.

The modem’s own sound card, where it has one, is given the lowest priority.session by dist/wireplumber/51-dolphone-bluetooth.conf, so it never becomes the default sink or source.

Ringing and tones

phoned synthesises its sounds into $XDG_RUNTIME_DIR/dolphone/ (or ~/.cache/dolphone/ without a runtime directory; never a shared directory, and always 0700) and plays them with pw-play:

  • the ring (400 ms on, 200 off, 400 on, 2 s off, at 400+450 Hz with a little of the octave so it carries from a small speaker), in the Notification role, on the profile’s codec_sink (the default sink may be an idle Bluetooth headset or the modem). While it rings the profile’s speaker_commands run, since a profile whose jack route mutes the speaker would otherwise ring silently; afterwards, and after a call, the codec goes back to the jack if a headset is in it and to the speaker if not. The Dialer runs the motor alongside, and PTT silences;
  • the call-waiting pips, in the ear: on the call’s output;
  • the text tone, when the ringer is on;
  • the alert tone for emergency alerts, in the Alarm role, whatever the ringer says. It is 853 Hz and 960 Hz together, the attention signal alert systems use. Its cadence is Wireless Emergency Alerts’ (47 CFR 10.520): a 2 s tone, then two 1 s tones, 0.5 s between each, the whole sequence twice with 0.5 s between, 10.5 s in all. It is the same in every country, as on Android, whose alert app plays one tone outside Japan; EU-Alert (ETSI TS 102 900) leaves the form to each country (ALERT_ATTENTION in phone-core/src/tones.rs);
  • the call-progress tones of the country the SIM (or the owner’s setting) says you are in: busy and congestion after the network turns a call away, and, for networks that send silence while ringing out, ringback (Phone Settings > Calls > Ringback on this phone). The cadences are ITU-T E.180’s, country by country, in phone-core/src/tones.rs. They play on the call’s output (after the call, the route’s), not the default sink;
  • the caller’s name, spoken with espeak-ng when “Say who is calling” is on. The name goes to espeak on its stdin, not its command line (which any user can read in /proc), and the speech is deleted once played.

Music playing on the machine (anything speaking MPRIS) pauses when a call rings and resumes when the last call ends.

Bluetooth headsets

dist/wireplumber/51-dolphone-bluetooth.conf turns on the hands-free Audio Gateway roles and PipeWire’s ModemManager integration, so a headset rings and its button works, and mSBC for wideband (16 kHz) call audio where the headset has it. Pairing goes through phoned’s BlueZ agent, which only accepts a pairing the owner started from a Dolphone screen in the last minute, and shows the passkey there.

Lessons kept

  • PipeWire clients in a systemd user unit with any sandboxing option (ProtectSystem, NoNewPrivileges, PrivateTmp) stall for seconds to 25 s. phoned’s unit has none.
  • pw-dump into a pipe can take seconds where the same dump to a file takes milliseconds; phoned dumps to a file, with a time limit.
  • Every command phoned runs for audio (mixer commands, pw-cli, pw-dump) has a time limit, since they run with the audio state locked and a stalled one would hold up the call.
  • In containers where RTKit cannot run (OrbStack), every PipeWire client waits out RTKit’s D-Bus timeout; the dev setup masks rtkit-daemon.

Devices

Dolphone is built for the Flipper One: its panel apps are FlipCTL apps, its buttons are the Flipper’s, and HARDWARE.md is the Flipper One’s bring-up. Underneath, though, phoned only needs what any Linux machine with a modem has: ModemManager for the modem, PipeWire for audio, BlueZ for Bluetooth, logind and D-Bus. So the daemon, phonectl and the desktop app run elsewhere too.

What differs between machines is the hardware around the modem, and that lives in a device profile: one TOML file that says how to recognise the machine and how its audio is wired.

Which one am I using?

phonectl device          # the profile in use, and why it was chosen
phonectl device list     # every profile phoned knows
phonectl device use pinephone     # choose one by hand (auto to detect again)

Phone Settings > About shows it on the panel too.

The profiles that ship

ProfileRecognised byStatusNotes
flipper-onedevice tree model “Flipper One”expectedThe reference. NAU8822 codec switched with amixer (Speaker, Headphone, from the kernel driver’s controls); the modem’s USB audio bridged by phoned. The board’s device-tree strings are not published yet, so detection is assumed: choose it by hand if it misses.
clockworkpi-uconsoledtoverlay=clockworkpi-uconsole in config.txtuntestedThe 4G extension is a SIMCom SIM7600G-H, which sends call audio as serial PCM. ModemManager’s SIMCom plugin turns it on (AT+CPCMREG=1) once a call connects and names the port; phoned bridges that port (or /dev/ttyUSB4) to PipeWire at the call’s rate, 8 or 16 kHz (SIMCom USB AUDIO Application Note V1.03). Needs you in the dialout group for the port.
raspberry-pidevice tree model “Raspberry Pi”untestedAny modem ModemManager supports. Mobile call audio needs a modem with a USB sound card; use a USB headset for the speaker and microphone.
pinephonepine64,pinephoneuntestedThe EG25-G is wired to the codec; ALSA UCM’s Voice Call verb connects it (call_start_commands); calls start on the earpiece.
librem5purism,librem5untestedThe BM818 is its own ALSA card (“Modem”), bridged to the codec by phoned, as on the Flipper One; calls start on the earpiece.
laptopSMBIOS chassis type (laptop, notebook, convertible), or ThinkPaduntestedThe desktop app. Whether the WWAN modem can call and text depends on it: mmcli -m 0 lists Voice and Messaging when it can. SIP works regardless.
genericnothing else matchedexpectedDefault devices, no mixer controls touched.

“Untested” means the values come from the sources each file cites (kernel device trees, alsa-ucm-conf, vendor documentation), but nobody has yet run the bring-up checks on that machine. Running them and reporting back, pass or fail, is the most useful contribution there is.

On the phones (PinePhone, Librem 5), stop callaudiod and GNOME Calls first: one program should own the modem’s calls and texts, and the package conflicts with GNOME Calls and Chatty for that reason.

Adding a device

Copy generic.toml (in /usr/share/dolphone/devices/ once installed, or dist/devices/ in the repository) to ~/.config/dolphone/devices/, give it an id, and fill in what you know:

id = "my-board"
name = "My Board"
status = "untested"              # tested once docs/HARDWARE.md passes on it
source = "where the values came from"
notes = "what works, what does not"
front_ends = ["desktop"]         # "flipctl" too, if it runs FlipCTL
ring_leds = []                   # names under /sys/class/leds to flash while ringing
modem_at = []                    # AT commands each time the modem appears
call_at_start = []               # AT commands when a mobile call connects
call_at_end = []                 # and when it ends (these four go above [match])

[match]                          # any one of these picks the profile
dt_compatible = ["vendor,board"]      # exact strings from /proc/device-tree/compatible
dt_model = ["My Board"]               # substrings of /proc/device-tree/model
dmi_vendor = []                       # substrings, on machines with DMI (x86)
dmi_product = []
dmi_family = []
dmi_version = []
dmi_chassis = []                      # SMBIOS chassis types: 9 laptop, 10 notebook...
file_contains = [["/etc/hostname", "myboard"]]
priority = 50                         # higher wins when several match

[at_port]                        # a spare AT port of the modem's (see below)
usb_vendor = "2c7c"                   # hex ids, as lsusb and sysfs write them
usb_products = ["0801"]
usb_interface = "03"                  # bInterfaceNumber

[audio]                          # any key of dist/audio.example.json
codec_sink = ""                       # PipeWire node names; empty for the default
mic_source = ""
speaker_commands = [["amixer", "-q", "sset", "Speaker", "unmute"]]
wired_commands = [["amixer", "-q", "sset", "Headphone", "unmute"]]
modem_match = ["Quectel"]             # how to spot the modem's USB audio nodes
call_start_commands = []              # a UCM verb, an AT command, an amplifier
call_stop_commands = []
earpiece_commands = []                # a phone's earpiece: calls start there when set
serial_pcm = ""                       # a modem's serial audio port, as on a SIM7600
assume_wired = false

Restart phoned (or phonectl device use my-board), place a call, and check with phonectl device and journalctl --user -u phoned. When it works, a pull request adding the file to dist/devices/ (and to the list in crates/phoned/src/devices.rs) brings it to everyone; the unit tests check that every profile parses, is described, and can be detected.

AT commands without ModemManager’s debug mode

modem_at, call_at_start, call_at_end, phonectl modem setup and the call forwarding and caller-ID codes are AT commands. ModemManager passes them only in its debug mode. A profile’s [at_port] names a spare AT port instead, by its place on USB, and phoned talks to it itself once udev tells ModemManager to leave it alone (ID_MM_PORT_IGNORE). The package’s /usr/lib/udev/rules.d/70-dolphone-at-port.rules does that for the Quectel RM520N-GL and RM500Q, interface 3, and gives the port to the dolphone group; it does nothing until:

sudo dolphone-modem-access port on   # port off undoes it; ModemManager restarts
sudo adduser $USER dolphone          # then log in again
phonectl modem setup                 # "at commands  ok  on phoned's own port /dev/ttyUSB3"

Only the Flipper One profile has an [at_port]; it is untested on a real modem. For another modem, find an AT port that ModemManager does not need as its primary one (mmcli -m any lists the ports and what it uses each for; the vendor’s Linux USB guide gives the interface numbers), copy the rule to /etc/udev/rules.d/ with the same name and change its ids and interface, and add [at_port] to the profile. phoned uses a profile’s port only when udev reports ID_MM_PORT_IGNORE on it, since two programs on one AT port garble each other’s answers. The at_port setting overrides: phonectl set at_port /dev/ttyUSB3 uses that port whatever udev says, and modemmanager never uses one.

Flashing LEDs for a ringing call

A profile’s ring_leds names LEDs under /sys/class/leds to flash while a call rings (the flash_on_ring setting, on by default). Their files belong to root, so the package’s /usr/lib/udev/rules.d/70-dolphone-leds.rules gives them to the dolphone group, for the Flipper One’s LEDs (flipper-one:*). Join the group once:

sudo adduser $USER dolphone      # then log in again

On another machine, copy the rule to /etc/udev/rules.d/, change its KERNEL match to your LEDs’ names (ls /sys/class/leds), and run sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=leds.

Where the facts are

  • cat /proc/device-tree/compatible | tr '\0' '\n' and /proc/device-tree/model on ARM boards; /sys/class/dmi/id/* on x86.
  • aplay -l and wpctl status for the sound cards and PipeWire’s nodes.
  • amixer scontrols for the mixer controls, or the codec’s driver in the kernel (sound/soc/codecs/).
  • /usr/share/alsa/ucm2/ for a UCM profile and its verbs.
  • mmcli -m 0 for what the modem offers, and lsusb for its USB functions (a “USB Audio” interface is what phoned can bridge).

Order of precedence

Built-in profiles, then /usr/share/dolphone/devices/, then ~/.config/dolphone/devices/ (a later file with the same id replaces an earlier one). The owner’s device_profile setting picks one by id; otherwise the best match is used. The owner’s own ~/.config/dolphone/audio.json still has the last word on any audio key.

Hardware bring-up: the go/no-go test

Everything in Dolphone runs against fake-mm. Whether it can be a phone on real hardware comes down to five gates. Run them on an RK3576 board from Flipper’s supported list (or any arm64 Debian 13 machine) with a Quectel RM520N-GL in an M.2 Key-B to USB adapter and a SIM with VoLTE. Until a Flipper One exists, that board running Flipper OS is the target.

GateQuestionPass
G1 Call setupDoes ModemManager place and receive VoLTE calls on your carrier?mmcli calls connect both ways
G2 Call audio (decides it)Can Linux hear and speak a call?two-way audio through a USB audio card the modem exposes (UAC), or a wired PCM path on a modem that has one
G3 TextsDo texts work over IMS on the same SIM?send and receive
G4 Carrier safetyWill the carrier accept the device?the IMEI is not blocked for lacking VoLTE emergency calling (Australia blocks such devices since the 3G shutdown: check with your carrier or ACMA)
G5 WakeDoes an incoming call wake the board from suspend?(can wait for the Flipper One itself)

GO when G1-G3 pass on at least one modem. NO-GO when G2 fails on every candidate: publish what was measured and stop.

On another machine (a Raspberry Pi, a uConsole, a PinePhone, a Librem 5, a laptop), the same gates are how its device profile goes from “untested” to “tested”: run them, note what passed, and send the profile’s file back with its status and notes updated (see DEVICES.md). phonectl device says which profile is in use, and why.

Before G0: prepare the board

Install Dolphone’s package on the board first: several gates use phonectl and the modem commands it brings, and the package also installs the polkit rule that lets your session use the modem, and dolphone-modem-access.

# A release's .deb, or one built on any arm64 Debian 13 machine. Without
# flipctl-slint's tools there, leave the panel bundles out:
SKIP_BUNDLES=1 scripts/package-deb.sh
sudo apt install ./target/deb/dolphone_*_arm64.deb    # answer yes: AT commands

Log in again so phoned starts with your session (systemctl --user status phoned). scripts/dev-setup.sh is for a machine without a modem: it refuses to run where it finds one, since it swaps ModemManager for the fake.

G0: the modem is there

sudo apt install modemmanager libqmi-utils
mmcli -L
mmcli -m any                       # model, firmware, state, signal
mmcli -m any --command='AT+QGMR' 2>/dev/null || sudo qmicli -p -d /dev/cdc-wdm0 --dms-get-revision

Write down the firmware revision: Quectel asks for it.

mmcli --command and Dolphone’s AT commands (the modem setup, the Flipper One’s AT+QPCMV=1,2 at every start, call forwarding codes) need ModemManager in debug mode, the only mode in which it passes AT commands from a client. Installing the package asks whether to turn it on (yes by default on a Flipper One, where call audio needs it). To change it later:

sudo dolphone-modem-access on      # or off, or status; dpkg-reconfigure dolphone asks again

Phone Settings > Modem offers Turn on too, with a password prompt.

What it costs: --debug also sets ModemManager’s log level to DEBUG. ModemManager hides numbers and message text from its QMI and MBIM log lines unless started with --log-personal-info (since 1.20), but not from AT traffic, so with an AT modem the journal can hold them. Removing the package turns it off again.

Or a spare AT port, with debug mode left off. The RM520N-GL (USB 2c7c:0801) has two AT ports: interface 2, which ModemManager makes its primary port, and interface 3, the “modem” port, which takes AT commands too and which ModemManager uses only as its secondary (Quectel’s UMTS/LTE/5G Linux USB Driver User Guide V3.1, Table 1; ModemManager’s 77-mm-quectel-port-types.rules). Dolphone can tell ModemManager to ignore interface 3 (the ID_MM_PORT_IGNORE udev tag) and send its commands there itself. Nobody has tried it on a real RM520N yet, so check it here:

ls -d /sys/bus/usb/devices/*/ttyUSB*           # which ttyUSB is which interface (…:1.3 is 3)
sudo dolphone-modem-access port on             # then log in again if you just joined dolphone
udevadm info -q property -n /dev/ttyUSB3 | grep ID_MM_PORT_IGNORE   # =1
mmcli -m any | grep -A4 " ports:"              # ttyUSB3 no longer listed; ttyUSB2 still (at)
ls -l /dev/ttyUSB3                             # crw-rw---- root dolphone
phonectl modem setup                           # "at commands ... on phoned's own port /dev/ttyUSB3"

Record: whether ModemManager still finds the modem, places and receives calls (G1) with the port gone, and still sees RING and new texts. Which port the modem sends those unsolicited messages to (AT+QURCCFG="urcport") is not in any RM520N manual found; if ModemManager misses them, answer AT+QURCCFG="urcport" on interface 3 and note it. If the interface numbers differ (another USB composition from AT+QCFG="usbcfg"), the rule and the profile’s [at_port] need the right one (DEVICES.md).

G1: calls

mmcli -m any --voice-create-call="number=0491570156"     # a number you own
mmcli -o <call> --start
mmcli -m any --voice-list-calls                          # watch ringing-out, active
mmcli -o <call> --hangup
# Incoming: ring the SIM from another phone, then
mmcli -m any --voice-list-calls
mmcli -o <call> --accept

If calls fail to set up: mmcli -m any | grep -i "carrier config" should name your carrier’s VoLTE profile (Quectel calls it MBN). AT+QMBNCFG="List" shows the profiles the firmware has.

Dolphone checks the setup a Quectel modem needs for calls and can make it:

phonectl modem setup           # USB audio, VoLTE, carrier profile: ok or change
phonectl modem setup apply     # waits for a press on the device; the modem restarts
StepChecked withChanged withSource
USB call audioAT+QCFG="usbcfg" (the last function is UAC)the same with it set to 1Quectel LTE Standard UAC Application Note §4.1
VoLTEAT+QCFG="ims" (<setting>,<registered>)AT+QCFG="ims",1Quectel forum (EC25, RM520N-GL threads)
Carrier profileAT+QMBNCFG="AutoSel"AT+QMBNCFG="AutoSel",1Quectel forum
RestartAT+CFUN=1,13GPP TS 27.007

None of the three is in the public RM5x0N AT manual (V1.0), so treat a “change” on real firmware as something to confirm, and report what it answered. Phone Settings > Modem shows the same check, with Set up when there is something to do.

G2: call audio (the deciding one)

The RM520N-GL has no PCM pins (they are reserved on the GL variant; only the -EU variant has them), so call audio has to come over USB as a sound card.

  1. Turn on UAC: phonectl modem setup apply, or by hand as Quectel’s LTE Standard UAC Application Note describes: query AT+QCFG="usbcfg", send the same values back with the last one set to 1, and restart (AT+CFUN=1,1). A firmware that lists only six functions cannot do it by AT command; ask Quectel for the RM520N-GL note and a firmware that can, with your revision.
  2. Send voice to USB: AT+QPCMV=1,2. The modem forgets it at every restart; the Flipper One profile’s modem_at has phoned send it each time the modem appears.
  3. Check a new card appears:
    aplay -l; arecord -l             # a Quectel card
    pw-dump | grep -i quectel        # its PipeWire nodes
    
  4. Make a call (G1) and route it by hand:
    pw-loopback --capture=<quectel source> --playback=<speaker or headset sink> &
    pw-loopback --capture=<microphone> --playback=<quectel sink> &
    
    Both people should hear each other. The note gives the format as 8 kHz, mono, 16-bit; PipeWire converts.
  5. Then let phoned do it: stop the two loopbacks, call again, and check pgrep -a pw-loopback shows the two phoned starts (names dolphone-downlink and dolphone-uplink). To find the Quectel card’s node names for the step above, wpctl status or pw-cli ls Node.

If the card appears but carries silence, try the other UAC mode the note lists, check AT+QAUDMOD, and capture pw-top during the call. Record what you tried.

Call forwarding and caller ID

ModemManager has no API for these, so the dialer sends them as the standard AT commands (3GPP TS 27.007), through the same debug-mode passthrough:

KeypadDoesCommand
**21*0412345678#forward every callAT+CCFC=0,3,"0412345678",129,1
**67*n#, **61*n#, **62*n#forward when busy, unanswered, unreachableAT+CCFC=1/2/3,3,...
**61*n**20#unanswered after 20 s...,1,,,20
*#21#is it on?AT+CCFC=0,2
#21#, ##21#, ##002#off, cleared, clear every forwardAT+CCFC=…,0 / ,4
*#31#, #31#, *31#caller ID: ask, show, hideAT+CLIR?, =2, =1
*43#, #43#, *#43#call waitingModemManager’s Voice API

Barring codes (*33*…#) are refused: they need the network’s barring password. On real hardware, after setting a forward, ring the SIM from another phone and check where the call goes.

G3: texts

mmcli -m any --messaging-create-sms="number=0491570156,text='Dolphone test'"
mmcli -s <sms> --send
mmcli -m any --messaging-list-sms        # after texting the SIM from another phone

On Australian 4G-only networks texts travel over IMS too: if G1 needed a carrier profile, G3 does as well.

Picture messages and group texts come over mobile data (MMS), so they need a data connection and mmsd-tng:

sudo apt install mmsd-tng                # it finds most carriers' MMS settings itself
# Send the SIM a picture from another phone, then:
phonectl threads                         # the text, with [Picture]
phonectl mms 0491570156 --file test.jpg "Dolphone MMS test"

If nothing arrives, check ~/.mms/modemmanager/mms against your carrier’s MMSC, proxy and APN, and journalctl --user -u mmsd-tng. Record what you changed.

Then: Dolphone on real hardware

Install the package (see install; scripts/package-deb.sh builds it on the board), which brings the polkit rule that lets phoned use Voice and Messaging from your session, and log in again:

phonectl status                          # your operator, signal, number
journalctl --user -u phoned -f           # what phoned makes of the modem

scripts/dev-setup.sh is for a machine without a modem: it refuses to mask ModemManager where it finds one.

On the Flipper One, ls /sys/class/leds shows the LEDs’ real names: if they are not the flipper-one:* names in the profile’s ring_leds, note them (and the udev rule’s match).

Report back

Open an issue titled Hardware test: <modem> on <board> with: modem and firmware, carrier and country, which gates passed, the commands that worked, and for G2 the card names and what you heard. A NO-GO report is as useful as a GO.

Developing

The machine

Dolphone builds and runs on arm64 Debian 13 (trixie), which is what Flipper OS is built on. On an Apple Silicon Mac, OrbStack runs that natively. On an arm64 Debian 13 machine (a Raspberry Pi, a cloud VM), scripts/dev-setup.sh --here sets up that machine itself instead. Elsewhere (an Intel Mac, x86 Linux) the daemon, the CLI, the desktop app and the end-to-end tests build and run, but the panel apps and their bundles need arm64.

scripts/dev-setup.sh        # creates `f1`, installs packages and Rust, builds FlipCTL
                            # (tracking its main branch), swaps in fake-mm

Your Mac’s home is visible inside the machine at the same path, so edit on the Mac and build inside. Keep build output inside the machine (CARGO_TARGET_DIR under ~): building on the shared folder is slower. After edits that make files longer, the machine can briefly see them cut short (“unclosed delimiter” at the end of a file); scripts/run-dev.sh drops the page cache first, or run sync; echo 3 | sudo tee /proc/sys/vm/drop_caches. Never run cargo fmt inside the machine right after a big edit on the Mac for the same reason.

Running everything

orb -m f1                     # a shell in the machine, at the same directory
scripts/run-dev.sh            # build, install to ~/.local, (re)start fake-mm, phoned, FlipCTL

Then open http://f1.orb.local:8899/: FlipCTL’s browser view, with the device’s buttons. Apps > Phone has the three apps.

phonectl fake ring 0491570156          # the phone rings; the Dialer comes up by itself
phonectl fake sms 0491570156 "hello"
phonectl fake answer | hangup | busy   # the far end
phonectl fake broadcast 4370 "test"    # an emergency alert
phonectl fake lock on                  # the SIM asks for its PIN (1234)
phonectl fake signal 30 5G             # FlipCTL's own status bar follows
phonectl watch                         # every event phoned sends
journalctl --user -u phoned -f

fake-mm is close enough to ModemManager that mmcli -m 0, mmcli -m 0 --voice-list-calls and FlipCTL’s status bar all read it.

Layout

WorkspaceBuild output (in the dev machine)
crates/*, apps/desktopthe root Cargo.toml$CARGO_TARGET_DIR (~/dolphone-target in the scripts)
apps/dialer, apps/messages, apps/settingseach its own (they link flipctl-app for the panel)~/dolphone-apps-target, or ~/dolphone-target/apps from bundle.sh

So cargo fmt, clippy and test at the root do not reach the panel apps: run them in each apps/<app> too (CONTRIBUTING has the list). Set CARGO_TARGET_DIR in the dev machine’s shell, or every build starts over in target/ on the shared folder. The cache dropping above is an OrbStack matter (its file sharing, virtiofs), not a Linux one.

Tests

The end-to-end scripts start their own phoned and fake-mm, and refuse to run beside another phoned: stop the one run-dev.sh started first (pkill -x phoned), and build first, since they run what is in $CARGO_TARGET_DIR/debug:

cargo build --workspace                      # what the scripts run
cargo test --workspace                       # core, daemon, CLI, desktop
scripts/e2e.sh                               # 224 checks: fake-mm, fake-mmsd, phoned, a SIP far end, phonectl, on real buses
scripts/ui-e2e.sh                            # the four apps driven live against phoned; screenshots in */target/render/live/
scripts/kdeconnect-e2e.sh                    # 10 checks against the real kdeconnectd (apt install kdeconnect)
cd apps/dialer && DOLPHONE_RENDER=1 cargo test  # behaviour, and every screen to target/render/
python3 tools/showcase.py                    # README pictures from the renders

scripts/e2e.sh also starts a second baresip on loopback (when baresip is installed) to be the far end of SIP calls, driven with phonectl fake sip ..., and runs phoned’s own baresip with a test tone for a microphone (DOLPHONE_SIP_TEST_AUDIO, debug builds only). It plays mmsd-tng with fake-mmsd (phonectl fake mms ...), encrypts and unlocks the store where systemd-creds --user works (systemd 256 and later; skipped in containers), and bridges serial call audio through a pty and the session’s real PipeWire when there is one.

scripts/ui-e2e.sh runs each app’s live_* test (ignored by a plain cargo test): the app is built as it runs on the device, its bus events are fed through its own event handler, keys go in through its own input path, and each step is rendered from its window. It waits as a person would, including the 400 ms after a call appears unasked, during which the Dialer ignores keys.

CI runs the unit tests, scripts/e2e.sh and scripts/ui-e2e.sh in Debian 13 containers on GitHub’s arm64 runners (the release Flipper OS is built on, so baresip and PipeWire’s tools are a board’s versions), with a system bus of the container’s own for fake-mm. A nightly job rebuilds the panel apps against flipctl-slint’s newest commit.

Panel bundles and the package

scripts/bundle.sh             # target/bundles/dolphone-*-aarch64.fap.AppImage (upstream's own tooling)
INSTALL=1 scripts/bundle.sh   # and into ~/Apps/Phone
scripts/package-deb.sh        # target/deb/dolphone_<version>_<arch>.deb (arm64 with the panel apps; amd64 without)
scripts/source-tarball.sh     # the GPL corresponding source for a release
scripts/gen-third-party-licenses.sh   # after any dependency change

Upstream

The panel apps depend on flipctl-app from flipctl-slint’s main branch by git. Move to upstream’s newest with cargo update -p flipctl-app -p flipper-ui in each app; the nightly job does this every night. A release records the exact upstream commit it was built against.

Conventions

The Flipper One comes first: a feature is designed for its panel and buttons, and other machines get it through their device profile and the desktop app. Hardware facts in a profile cite their source. Comments explain why, in full sentences. Every panel position is computed in Rust and text is measured in the font that draws it. Nothing blocking runs on an async thread. A user-facing string says what to do, not what went wrong inside.

Security

A phone can spend money, leak one-time codes and be used to harass. Dolphone runs on a device whose owners install a lot of third-party tools, so it assumes some of what runs beside it is careless or hostile.

Threat model

ThreatWhat Dolphone does
A program as the same user texts or dials (premium numbers, toll fraud, spam)Anything not from Dolphone’s own front ends waits for a press on the device or in the desktop app. Premium numbers (AU 19xx) and short codes wait for everyone. Texts are limited to 30 an hour and calls to 20.
A program texts without asking, once allowedA standing grant is for one program, listed numbers or a prefix, a per-hour limit (1–60) and an end date (up to a year, or none), allowed on the device. A grant names a program by its executable’s name, not its path: a grant to phonectl covers every script that runs phonectl. Premium, short and emergency numbers can never be granted. Anyone may revoke a grant; only Dolphone’s own apps may list them.
A program changes your contacts (to make a scammer’s number read “Bank”, or to get past “silence unknown callers”)Adding, editing, removing or importing contacts from anything but Dolphone’s own apps waits for a press on the device. Reading contacts needs the same grant as reading messages. A phonebook copy over Bluetooth is only started from Phone Settings. Imports keep names and numbers only.
A program reads your one-time codesMessages, the call log and the block list are only served to Dolphone’s own apps and to programs the owner grants reading (read_grants). Signals carry no text, and the numbers in them go in full only to those same programs (registered with Settings.Register); everyone else gets them masked. Same-user code can still read the store file directly; see Limits.
A program answers a call it placed (the device as a room microphone), sends keypad tones, reroutes audio, pairs a Bluetooth headset or changes settingsOnly Dolphone’s own apps may. Anyone may hang up.
A program poses as the desktop’s notification server (to read notifications or press their buttons)phoned only uses a notification server that is a system program (under /usr, owned by root), only acts on button signals from that server, and no notification can allow a waiting request: Review opens the desktop app, and only Deny acts directly.
Someone on the network uses FlipCTL’s browser viewThat view has no login upstream: anyone who can reach it sees the screen and can press keys, including Allow. Keep it off, or bound to localhost, on untrusted networks; it is worth raising with Flipper on Discord.
A hostile text (control characters, bidi overrides that make a link read backwards, zero-width text, huge bodies)Cleaned once on arrival, before the store, logs or screens; capped at 4 KB; drawn with ASCII fonts. Flash (class 0) texts are shown, not stored.
USSD codes that change account settingsOnly run when typed on Dolphone’s own dialer; never from D-Bus callers, never from links.
Emergency callsNever blocked or delayed when dialled on the keypad; never sent over the internet; routed to the modem as normal calls, which the network treats as emergency calls. An outside program asking to call 000 must be confirmed.
Personal data outliving a resetThe store, contacts and MMS pictures live under ~/.local/share/dolphone (0700/0600). /home survives a Flipper OS factory reset, so Settings has Delete phone data, confirmed on the device. It removes messages, calls, alerts, grants, the audit log, blocked numbers, settings, MMS pictures and contacts. It leaves the SIP account (~/.config/dolphone/sip/), rules, KDE Connect pairings, the gateway token and the store’s key; delete ~/.config/dolphone for those.
A copy of the home directory (a backup, another account, a disk read elsewhere)phoned --encrypt-store encrypts the message store with SQLCipher under a random key that systemd-creds --user seals to this user on this machine (with the machine’s root-only credential secret, and the TPM2 chip when there is one). A copy of the home directory alone cannot open it. Someone with the whole disk can, unless a TPM holds the secret or the disk is encrypted. If the key cannot be unsealed phoned leaves the store as it is and runs on an empty one in memory: new texts stay on the SIM, but until the key is back the block list, grants, speed dial and other settings are at their defaults, and calls are not logged. MMS pictures go into the store with the texts, and while an app shows one it is a copy in $XDG_RUNTIME_DIR (memory, gone at logout). Contacts (.vcf files) are not encrypted with it.
Someone holding the unlocked device, or at its desktop, reads your textsA Messages PIN (4 to 12 digits, Phone Settings or phonectl set messages_pin) keeps Messages closed until it is entered, and new texts’ notifications without their text; it stays open for messages_open_for seconds (5 minutes by default). It is kept as a salted PBKDF2 hash; after five wrong guesses each one waits, from 30 s, doubling. It does not hold back programs the owner granted reading, or same-user code (see Limits).
A program sends your files by MMSSendMms from anything but Dolphone’s own apps waits for a press on the device, which shows the files’ names; grants never cover MMS. Files are read only once allowed, and at most 8 MB.
ModemManager’s debug modeDolphone’s AT commands (the modem’s setup, call forwarding codes, the Flipper One’s call-audio command) pass only in this mode, which also logs at debug level: ModemManager hides numbers and texts in its QMI and MBIM log lines, but not in AT traffic. It also lets any program in your session send the modem AT commands through ModemManager (mmcli --command; the polkit rule allows device control to the active local session), which goes around every confirmation here: dialling, texting, forwarding. The install asks (yes by default only on a Flipper One, where call audio needs it); dolphone-modem-access off turns it off, and removing the package does. Through Dolphone, call forwarding codes still only run from its own dialer.
The spare AT port (instead of debug mode)dolphone-modem-access port on has ModemManager ignore one AT port of the modem (ID_MM_PORT_IGNORE) and gives that tty to the dolphone group, mode 0660 (70-dolphone-at-port.rules); without it the port is root’s and the dialout group’s, as before. phoned runs as you (a systemd user service), so you must be in the group, and then so is every program running as you, and every other member: any of them can open the port and send the modem AT commands, around Dolphone’s confirmations, as with debug mode. What it does not do is raise ModemManager’s log level. phoned uses a profile’s port only when udev says ModemManager ignores it, so the two never share one. Keep the group to the phone’s owner; port off gives the port back to ModemManager, and removing the package does.
A program changes a setting (who may read messages, the device profile, 2G)Settings.Set is for Dolphone’s own apps; anything else asks with RequestSetting, which waits for a press on the device showing the setting and its new value. The SIP password is never set that way.
A laptop over KDE ConnectIt listens on the local network (UDP and TCP 1716–1764). Pairing is always confirmed on the device, and the approval pairs only the connection whose certificate asked; that certificate is remembered and must match. Requests are limited per device and per address, lines and connections are capped, and handshakes time out. A paired laptop reads your texts and texts as you (premium numbers and short codes still ask); with private notifications on, new texts are not pushed to it. Unpair it in Phone Settings > Laptops.
The SMS gatewayOff unless started (systemctl --user enable --now dolphone-gateway). It listens on 127.0.0.1; any other address needs --insecure-listen, since it speaks plain HTTP and the token would cross the network in clear. Every request needs the token in ~/.config/dolphone/gateway.token (0600), requests and connections are limited, and it reads only with its own read grant.
LogsNumbers are masked (+61…156); message bodies are never logged.
Knowing what programs didEvery outside request (dial, text, read, contacts, grants, answer attempts) goes in an audit log in the store, numbers masked, capped at 1000 entries, readable only from Dolphone’s own apps (Phone Settings > Privacy).
A rule on incoming texts as a way inRules live in ~/.config/dolphone/rules.json; unknown fields make the file invalid rather than quietly ignored. A rule runs a program directly (never a shell) with the text as JSON on stdin, for at most 30 s; it may reply only to a sender it names, never to “anyone”, and a rule can require a shared secret in the text. Texts that arrive over SIP never run rules: a SIP sender is not authenticated the way a mobile network’s is.
Someone who can reach baresip directly (the local network, by default)A SIP peer counts as a phone number, and so as a contact, only when it comes from the account’s own domain or proxy; from anywhere else it stays a SIP address, in a thread of its own. Texts over SIP are limited to 30 an hour.
A program poses as mmsd-tngphoned uses the MMS service only when the program owning its bus name is a system program (under /usr, owned by root), as for the notification server.
The SIP accountThe password is written to ~/.config/dolphone/sip/ (0700, file 0600) and never returned over D-Bus (Get answers “set”). Only Dolphone’s own apps can set it. The account and proxy are checked so they cannot add options to baresip’s account line. phoned listens only to the baresip process it started, so another program claiming baresip’s bus name cannot fake calls. Media is encrypted (SRTP) when the provider offers it. Emergency calls never go over SIP.
Emergency alertsNational alerts cannot be turned off. Alerts are shown and sounded whatever the ringer says, and their text is always shown (they are public).
BluetoothThe pairing agent accepts only the device picked on a Dolphone screen, for a minute; any other device asking then, a keyboard included, is refused.

Limits

“Dolphone’s own front end” is decided by the executable behind a D-Bus caller, by full path: /usr/bin/dolphone-* owned by root, or the same name inside an AppImage’s FUSE mount (the panel apps run from ~/Apps). The DOLPHONE_TRUST_ALL and DOLPHONE_READ_GRANTS test switches exist only in debug builds. This stops scripts and tools that use the API, and makes every such action visible, but it is a speed bump. Code running as your user can still:

  • mount a FUSE filesystem of its own holding a program named like a front end (unless the trust_appimages setting is off, which trusts only /usr/bin);
  • connect to the bus and then run a real front end on that connection (the bus records the process that connected, and after exec it is the real program);
  • watch the whole session bus (dbus-monitor), which shows message text on its way to the apps, notification bodies and pairing passkeys;
  • write Dolphone’s own files: the store (grants, settings), rules.json, KDE Connect’s paired certificates, contact cards;
  • read the store file (or, with it encrypted, unseal its key the way phoned does), ptrace the apps, or call ModemManager directly (polkit allows the active local session).

Signals carry no message text, and their numbers in full only to registered programs, but a bus monitor sees those too. Separating users, or a sandbox around third-party tools, is the real fix; ROADMAP.md lists the next steps.

Reporting

Use GitHub’s private vulnerability reporting: the repository’s Security tab, then Report a vulnerability. Please do not open a public issue for something that lets a program text, dial or read messages without confirmation.

Roadmap

Ten reviews of Dolphone (as an everyday phone, for Flipper power users, by Linux phone veterans, for accessibility, messaging, telephony, desktop integration, reliability, privacy, and “does it feel like a phone on this machine”) produced a long list. All of it that can be built without the hardware is built: see CHANGELOG.md. This is what is left, and why.

0. A real call on a Flipper One

Everything has been tested against stand-ins: fake-mm for ModemManager, fake-mmsd for mmsd-tng, a second baresip for SIP, a pty for a SIM7600’s serial audio. docs/HARDWARE.md is the go/no-go test on the hardware; its deciding question is whether the RM520N-GL’s USB audio gives Linux two-way call audio. What only a board can confirm:

  • The Quectel commands. phonectl modem setup checks USB audio (AT+QCFG="usbcfg"), VoLTE (AT+QCFG="ims") and the carrier profile (AT+QMBNCFG="AutoSel"), and the Flipper One profile sends AT+QPCMV=1,2 at every start. They come from Quectel’s UAC application note for its LTE modules and from Quectel’s forum, not from the RM520N’s own AT manual, so their answers on real firmware are the first thing to record.
  • The Flipper One itself: its device-tree strings (the profile assumes them; phonectl device use flipper-one overrides), the names of its four LEDs, and how its RP2350 reports the headset jack.
  • MMS on a carrier: mmsd-tng fetches over mobile data with the carrier’s MMSC and APN, which it mostly finds by itself; nothing here has met a real MMSC.
  • AT commands without ModemManager’s debug mode. Built: a spare AT port that ModemManager is told to ignore (ID_MM_PORT_IGNORE), which phoned talks to itself (sudo dolphone-modem-access port on; the Flipper One profile names the RM520N-GL’s interface 3). Tested only against a pty. A board must confirm that interface 3 is an AT port on the RM520N’s firmware, that ModemManager still calls, texts and sees RING without it (which port gets the modem’s unsolicited messages, AT+QURCCFG="urcport", is not in any manual found), and the numbers under other USB compositions.

1. Optional, later

  • The panel outside Dolphone’s apps: a status-bar badge for missed calls and texts, a toast, a home tile; a large-text mode and a font beyond Latin and Cyrillic (Dolphone draws with FlipCTL’s fonts, and shows the number where a name cannot be drawn). FlipCTL has none of these today; Dolphone can add them as options of its own later, without waiting for FlipCTL.
  • No RCS: there is no route to it from Linux. Bridges to Matrix and others belong outside Dolphone, on its D-Bus API.

2. Not built yet

Nothing else that can be built without the hardware.

Contributing

Thanks for wanting to help. The most valuable thing right now is hardware: if you have an RK3576 board and an M.2 modem, run docs/HARDWARE.md and report what happened, GO or NO-GO. On another machine with a modem (a Raspberry Pi, a uConsole, a PinePhone, a Librem 5, a laptop), running the same checks moves its device profile from “untested” to “tested”, and a new machine is one TOML file: docs/DEVICES.md.

Ground rules

  • Add to FlipCTL, never replace it. Screens use FlipCTL’s widgets, fonts and soft-key roles (docs/BUTTONS.md). If something should change in FlipCTL itself, that is a contribution to flipctl-slint, not a workaround here.
  • The Flipper One comes first. A feature is designed for its panel and its buttons; other machines get it through their device profile and the desktop app. Hardware facts in a profile cite where they came from.
  • phoned stays the one owner of calls, texts and call audio. Front ends talk to it, never to ModemManager or PipeWire.
  • Tests come with changes. Logic in model.rs files is tested directly; screens are rendered (DOLPHONE_RENDER=1 cargo test) and looked at; daemon behaviour goes in scripts/e2e.sh; a new flow through an app goes in its live_* test (scripts/ui-e2e.sh).
  • Run cargo fmt, cargo clippy --all-targets -- -D warnings and the tests before a pull request; CI runs the same.

AI-assisted contributions

Welcome, on the terms Flipper Devices sets for its own repositories: you must understand, test and be able to defend every line yourself, and answer review yourself. Pull requests that relay a tool’s output unread will be closed.

Talking to Flipper Devices

Dolphone is unofficial. Please do not open issues on Flipper’s repositories about Dolphone; their contribution rules ask for discussion on Discord and for designs as comments on their “help wanted” tasks. Changes we want upstream go there as small, self-contained pull requests.

Conduct

Be kind and assume good faith; see CODE_OF_CONDUCT.md.

Changelog

0.1.0 (unreleased)

First version, built for the Flipper One, tested against fake-mm, a SIP far end and in the real FlipCTL; not yet on a real modem.

The roadmap from the reviews, done:

  • It rings: a logind inhibitor while a call exists; catching up after resume; the modem allowed to wake the board (udev); ModemManager’s signals instead of polling; calls adopted after a restart in any state; SIM PIN entry on the keypad; a monotonic clock for durations, limits and expiry.
  • The panel: missed calls and new texts beside the operator; the touchpad as a keypad; T9 name matching; speed dial (hold 2–9, assigned by holding Ok on a contact); voicemail (hold 1, with carrier numbers from the SIM); quick replies in conversations; a vibration for each kind of news; hold PTT to switch the ringer; “hold to decline”; the TTY relay hint for 106; a roaming note.
  • Telephony: call waiting on and off; conference (hold Ok to merge); the region from the SIM; the country’s busy, congestion and ringback tones.
  • Calls over the internet: SIP through baresip, with the same screens, routes and log.
  • Messaging: emergency alerts (cell broadcast) full screen; delivery reports; resending a failed text; emoji joiners kept.
  • Contacts: copied from a paired phone over Bluetooth (PBAP) or imported from a vCard export, merged rather than doubled, names and numbers only; managed from the desktop app and phonectl contacts.
  • Scripts: standing grants; rules on incoming texts; an audit log; phonectl --json, watch as NDJSON, send --wait, otp --wait.
  • The desktop: Copy and “Copy code” for one-time codes (cleared after a minute); keyboard shortcuts; the alert card; every new setting.
  • Privacy: prefix blocking; silence for unknown callers.
  • Device profiles: the Flipper One first, and the ClockworkPi uConsole, a Raspberry Pi, the PinePhone, the Librem 5, laptops and a generic fallback; an amd64 package.
  • Picture messages and group texts: MMS through mmsd-tng (pictures kept beside the store, shown in the desktop app, named on the panel; a group is one thread and a reply goes to everyone); phonectl mms and SendMms to send, waiting for the device like a text when a program asks; fake-mmsd to test against.
  • Carrier services: call forwarding, caller-ID and call-waiting codes from the keypad (3GPP TS 22.030, sent as AT+CCFC and AT+CLIR); barring codes refused.
  • The modem: phonectl modem setup and Phone Settings > Modem check a Quectel modem’s USB call audio, VoLTE and carrier profile and set them with one press (it restarts); a device profile can send AT commands when the modem appears and when a call starts or ends. The install asks whether ModemManager may pass them (its debug mode), and dolphone-modem-access on|off changes it. Or, without debug mode, phoned sends them on a spare AT port that ModemManager is told to ignore (dolphone-modem-access port on, a udev rule for the RM520N-GL’s interface 3, the profile’s [at_port], the at_port setting); phonectl modem setup says which way they go. Tested against a pty in fake-mm, not a modem.
  • Call audio: a SIM7600’s serial PCM bridged to PipeWire at 8 kHz (the uConsole’s 4G extension); an earpiece route for the PinePhone and Librem 5; holding and swapping between a SIP call and a mobile call.
  • Texts: search (FTS5) and export as SMS Backup & Restore XML; receiving texts over SIP; the SIM each text and call went through.
  • The panel: a Today screen of missed calls, unread threads and favourites; an adjustable long-press time; the LEDs flash for a ringing call (a udev rule gives the dolphone group the Flipper One’s LEDs).
  • Cell information: a log of the networks the modem was on, a warning when it is forced onto 2G, and a switch to refuse 2G.
  • The laptop: KDE Connect (texts and call alerts in GSConnect, replies from it), a tray icon and launcher badges for the desktop app.
  • Scripts: an optional SMS gateway (HTTP, localhost by default); a text when the uplink goes down.
  • Privacy: the message store can be encrypted with SQLCipher (phoned --encrypt-store), its key sealed by systemd-creds --user; without the key it stays locked and texts stay on the SIM. Trusting AppImage front ends can be turned off. A program asking to change a setting (phonectl set, phonectl device use) waits for a press on the device, as texts and calls do; Delete phone data removes contacts too. The panel apps no longer link the database at all.
  • Tests: the end-to-end suite grew to 224 checks, with 10 more against the real KDE Connect daemon; the four apps are driven live against phoned (scripts/ui-e2e.sh); CI runs in Debian 13 containers; the suite also passes with every CPU core busy.

Fixed after twenty more reviews (security from a program and from the network, robustness, concurrency, data, packaging, the modem, audio, emergency calling, telephony standards, numbers abroad, accessibility, the scripting API, the tests, onboarding, FlipCTL’s conventions, licensing, battery, failure recovery, privacy):

  • Security: an AppImage front end must be a real FUSE mount; a SIP peer is a contact’s number only from the account’s provider, and SIP texts run no rules; Bluetooth pairing accepts only the device picked; KDE Connect binds approval to the asking certificate, caps lines, connections and requests, and times out handshakes; the gateway refuses non-loopback addresses without --insecure-listen, caps connections and has deadlines; mmsd-tng must be a system program; notifications escape markup; grant prompts say which numbers, how often and for how long; live calls’ numbers and others’ requests are masked for programs that may not read; the SIM’s PIN and PUK codes are handled on the device instead of sent as USSD.
  • Emergency: national numbers for NZ, India, Singapore, France, Russia, Brazil, China, South Africa; +61 000 and the like; an emergency call with the SIM locked; Australia’s Emergency Alert texts (0444 444 444) never blocked and sounded through silent; alerts shown even when they cannot be stored; channel 919.
  • Calls and texts: a call adopted after a restart gets its audio; a call that can no longer be followed ends instead of making later calls “waiting”; a ModemManager restart or a pulled SIM ends the old calls; audio starts and stops in order; SIP crashes are noticed; Bluetooth calls switch the headset to its hands-free profile; the speaker route and the ringer play where they should; caller-ID hiding survives a modem restart; delivery reports wait out temporary errors; segment counts match how parts are packed; alphanumeric senders keep their names; two different international numbers are never one contact.
  • Data: a store is moved aside only when damaged, with its WAL; contact edits keep every other line of a card and write atomically; MMS pictures cannot overwrite each other and are kept by mmsd-tng until all are saved; texts left sending become failed and can be sent again.
  • Packaging and licences: the LED udev rule works (it never ran its commands); upgrades keep ModemManager running and the owner’s debug-mode answer; SQLCipher’s notice, the desktop app’s GPL, the AppImage runtime’s notices; the source tarball builds offline.
  • Battery: the apps redraw and poll only when something changed; contacts are read once; phoned wakes less when idle.

The rest of that roadmap:

  • Signals with numbers in them go in full only to Dolphone’s own apps and programs granted reading (they register with Settings.Register); everyone else gets the number masked.

  • A Messages PIN (4 to 12 digits, kept as a PBKDF2 hash): Messages and its notifications stay closed without it, it opens for a few minutes at a time, and wrong guesses slow down after five.

  • MMS pictures go into the encrypted store with the texts (--encrypt-store moves them in, --decrypt-store back out).

  • Text-to-911 in the US (47 CFR 9.10(q)); texts to emergency numbers elsewhere are still refused.

  • phonectl exits with a code that says why (2 not allowed, 3 bad argument or not supported, 4 rate-limited, 5 phoned not running), and --json for get, ussd and what it sends.

  • The uplink watch, KDE Connect and the headphone jack wake on changes instead of every few seconds.

  • A PIN screen in Messages (panel and desktop) and PIN settings in both Settings.

  • Emergency alerts queue, the most severe first (“1 of 3”); the attention tone and its vibration are the 10.5 s pattern of 47 CFR 10.520, everywhere.

  • Pictures can be attached from the apps: the desktop app through the file-chooser portal, panel Messages from a list of the newest in Pictures and Downloads. A reply from the panel to a group now reaches everyone in it (it went to the first number only).

  • Serial call audio keeps the modem’s clock and PipeWire’s apart with a small buffer that drops or pads whole frames, so a long call does not drift.

  • phoned: calls (dial, answer, decline, hold/swap, waiting, DTMF, mute), texts (multipart, cleaning, deletion from the modem once stored), confirmations for outside requests and premium numbers, rate limits, blocking, USSD, call log, PipeWire call audio with echo cancellation, ringer, jack and headset buttons, Bluetooth (BlueZ agent), desktop notifications.

  • Panel apps for FlipCTL: Phone, Messages, Phone Settings.

  • dolphone-desktop for desktop mode, with tel: and sms: links.

  • phonectl, fake-mm, an end-to-end suite, a Debian package and CI.

Fixed after ten reviews, before release:

  • Security: reading messages and the call log, answering, keypad tones, audio routing, Bluetooth, the block list and settings are limited to Dolphone’s own apps (and, for reading, to granted programs); signals carry no message text; trust is by full executable path; the trust-everything test switch is gone from release builds; the notification server is verified and notifications cannot allow a request; private notifications are the default.
  • Texts: the network’s timestamp is kept; duplicates are not stored twice; a text is deleted from the modem only once it is stored; delivery reports are not stored as texts; MMS notifications are left to mmsd-tng (a placeholder without it); flash texts are shown; multipart texts are waited for up to ten minutes.
  • Calls: audio starts while an outgoing call rings (early media); a call that ended while phoned was down is logged as missed; every call ends cleanly when the modem goes away; the ringer stops after 90 s; supplementary-service codes are run as such instead of sent as USSD.
  • Modem: a locked SIM or disabled modem is reported (and a disabled modem enabled); watchers restart when a subscription fails; SIGTERM is handled; an unreadable store is moved aside instead of stopping phoned; the store uses WAL with full sync.
  • Reliability, found by running the suites with every core busy: a ModemManager lookup that times out is no longer taken as the modem leaving (which ended its calls); commands to baresip go one at a time, so “callfind” and the command after it cannot be split by another call’s; a BlueZ that is installed but not running is not woken on every Bluetooth question (each waited out the bus’s activation timeout, stalling Phone Settings); otp --wait sees a code that arrives while it is still connecting; baresip is restarted at once after a crash; the tests wait for states instead of sleeping, and their SIP audio uses ALSA’s null device rather than baresip 1.1.0’s aufile player, whose thread can outlive the player it plays for.
  • Panel: names the panel cannot draw show as the number.
  • Desktop: screen-reader roles and labels on every custom control; contrast raised to WCAG AA for secondary text and your own bubbles.