#!/usr/bin/env osh
shopt -s strict:all
DEP="polkit modemmanager linuxconsoletools pinephone-call-audio pn"
DEC="Manages phone calls / text messages using modemmanager"
DOC="
A single interactive prompt-driven script to manage all aspects of
using your phone's modem for sending & receiving text messages and
placing and receiving calls.
Transfers incoming text messages and logs calls and all actions to
a single logfile which can be interactively filtered (per-contact
etc..) via using the provided phonelog submenu. The log file used
can be customized by setting \$F_PHONE_MODEMHISTORYFILE. Additionally
a number of hooks can be set, see variables for usable.
The script in the background (via a subshell), continually monitors
modemmanager (via its DBus interface via mmcli), alerts you when a
new call is incoming (vibrating the phone and interrupting your
current prompt to provide a pickup prompt).
So in short, basically, this script allows you to use pinephone (or
otherwise) as , who would have thought.. a lightweight... phone(!)
"
VAR="
F_PHONE_MODEMHISTORYFILE=~/.f_phone_modemhistory
F_PHONE_MONITORSTATEFILE=/tmp/f_phone_monitorstate
F_PHONE_HOOKRINGSTART='yes 5 | fftest /dev/input/by-path/platform-vibrator-event'
F_PHONE_HOOKRINGSTOP='pkill fftest'
F_PHONE_HOOKMISSEDSTART='echo 255 > /sys/class/leds/green:indicator/brightness; { echo 5; sleep 1; echo -1; } | fftest /dev/input/by-path/platform-vibrator-event'
F_PHONE_HOOKMISSEDSTOP='echo 0 > /sys/class/leds/green:indicator/brightness'
F_PHONE_COUNTRYCODE='US'
"
cleannumber() { pn find -c "$F_PHONE_COUNTRYCODE" "$1"; }
mm() { mmcli -m 0 "$@"; }
monitorstateget() { cat "$F_PHONE_MONITORSTATEFILE" || echo ""; }
monitorstateset() { echo "$1" > "$F_PHONE_MONITORSTATEFILE"; }
ringhangup() { mm -o "$(callid "incoming.+ringing")" --hangup; }
ringpickup() { loopincall --accept "$(callid "incoming.+ringing")"; }
promptok() {
[ -p /tmp/fbp.fifo ] && echo -e "\b\f\rok" > /tmp/fbp.fifo
read -p Ok foo
}
errdie() {
echo "$1"
terminate
}
terminate() {
[ -n "$GLOBAL_AUDIOSAVE_FILE" ] && alsactl --file "$GLOBAL_AUDIOSAVE_FILE" restore
[ -n "$GLOBAL_CALLID_INPROGRESS" ] && mm -o "$GLOBAL_CALLID_INPROGRESS" --hangup
eval "$F_PHONE_HOOKMISSEDSTOP" >&2 2>/dev/null &
eval "$F_PHONE_HOOKRINGSTOP" >&2 2>/dev/null &
kill 0
exit 1
}
installpolkitudevrules() {
TARGETPOLKITFILE="/etc/polkit-1/rules.d/00-f_scripts-fphone-mmplugdev.rules"
TARGETUDEVFILE="/etc/udev/rules.d/00-f-scripts-f-phone.rules"
test -f "$TARGETPOLKITFILE" || {
echo "Installing polkit rule for modemmanager (for group plugdev)"
sudo mkdir -p "$(dirname "$TARGETPOLKITFILE")"
echo '
polkit.addRule(function(action, subject) {
if (
action.id.indexOf("org.freedesktop.ModemManager1.") == 0 &&
subject.isInGroup("plugdev")
) { return polkit.Result.YES; }
});
' | sudo tee "$TARGETPOLKITFILE"
sudo chmod a+rx "$TARGETPOLKITFILE"
sudo addgroup "$USER" plugdev
clear
echo "polkit rule installed! you may need to reboot"
promptok
}
test -f "$TARGETUDEVFILE" || {
echo "Installing udev rule for setting system LED (for group video)"
sudo mkdir -p "$(dirname "$TARGETUDEVFILE")"
echo '
ACTION=="add", SUBSYSTEM=="leds", RUN+="/bin/chgrp video /sys/class/leds/%k/brightness"
ACTION=="add", SUBSYSTEM=="leds", RUN+="/bin/chmod g+w /sys/class/leds/%k/brightness"
' | sudo tee "$TARGETUDEVFILE"
sudo addgroup "$USER" video
sudo chmod a+rx "$TARGETUDEVFILE"
clear
echo "udev rule installed! you may need to reboot"
promptok
}
}
ensuremmrunning() {
rc-service modemmanager status | grep started || {
sudo /etc/init.d/modemmanager start
sudo rc-update add modemmanager boot
}
}
maketext() {
local MSG NUMBER ACTION
echo "maketext: Dialog for sending a text message"
while true; do
if [ -z "$NUMBER" ]; then
{ echo -e "\b\f\e"; seq 0 9; echo -e "\\\\n"; } > /tmp/fbp.fifo
read -p "Number: " NUMBER
fi
if [ -z "$MSG" ]; then
echo -e "\a" > /tmp/fbp.fifo
read -p "Message: " MSG
fi
echo "Will send to <$NUMBER>, the message: <$MSG>"
OPTS="send cancel editmsg editnum"
[ -p /tmp/fbp.fifo ] && { echo -e "\b\f\r"; echo "$OPTS" | tr " " "\n"; } > /tmp/fbp.fifo
read -p "Action: " ACTION
[ "$ACTION" == send ] && break
[ "$ACTION" == editmsg ] && MSG=''
[ "$ACTION" == editnum ] && NUMBER=''
[ "$ACTION" == cancel ] && return
[ "$ACTION" == interrupt ] && return
done
NUMBER="$(cleannumber "$NUMBER")"
logevt textsend_attempt "$(date +%s)" "$NUMBER" "$MSG"
echo "Sending text message to <$NUMBER>: <$MSG>"
TEXTID="$(
mm --messaging-create-sms="text=\"$MSG\",number=$NUMBER" |
grep -Eo "SMS/[0-9]+" | cut -d/ -f2
)"
mm -s "$TEXTID" --send
logevt textsend_success "$(date +%s)" "$NUMBER" "$MSG"
promptok
}
volset() {
amixer sget "$GLOBAL_EARPIECE" | grep -qE '\[on\]' && amixer set "$GLOBAL_EARPIECE" $1 > /dev/null
amixer sget "$GLOBAL_HEADPHONE" | grep -qE '\[on\]' && amixer set "$GLOBAL_HEADPHONE" $1 > /dev/null
amixer sget "$GLOBAL_SPEAKER" | grep -qE '\[on\]' && amixer set "$GLOBAL_SPEAKER" $1 > /dev/null
}
logevt() {
{
FIRST=1
for ARG in "$@"; do
if [ "$FIRST" = 1 ]; then
FIRST=0
else
printf %b '\t'
fi
printf %b "$ARG"
done
printf "%b" "\n"
} >> "$F_PHONE_MODEMHISTORYFILE"
}
makecall() {
local NUMBER ACTION
echo "makecall: Dialog for placing a call"
while true; do
if [ -z "$NUMBER" ]; then
{ echo -e "\b\f\e"; seq 0 9; echo -e "\\\\n"; } > /tmp/fbp.fifo
read -p "Number: " NUMBER
fi
echo "Will initiate a call with <$NUMBER>"
OPTS="confirm cancel editnum"
[ -p /tmp/fbp.fifo ] && { echo -e "\b\f\r"; echo "$OPTS" | tr " " "\n"; } > /tmp/fbp.fifo
read -p "Action: " ACTION
[ "$ACTION" == confirm ] && break
[ "$ACTION" == editnum ] && NUMBER=''
[ "$ACTION" == cancel ] && return
[ "$ACTION" == interrupt ] && return
done
NUMBER="$(cleannumber "$NUMBER")"
logevt callsend_attempt $(date +%s) "$NUMBER" "$MSG"
CALLID="$(
mm --voice-create-call "number=$NUMBER" |
grep -Eo "Call/[0-9]+" | cut -d/ -f2
)"
loopincall --start "$CALLID"
}
modemrst() {
mmcli -L | grep /org/freedesktop/ModemManager1 ||
sudo /etc/init.d/modemmanager restart
while ! mmcli -L | grep /org/freedesktop/ModemManager1; do
echo "Waiting for modem to come online"
sleep 1
done
}
callid() {
TYPE="$1"
mm --voice-list-calls -a |
grep -v terminated |
grep -Eo "Call/[0-9]+.+$TYPE" |
grep -Eo '[0-9]+'
}
lookupnumberfromcallid() {
VOICECALLID="$1"
cleannumber "$(
mm --voice-list-calls -o "$VOICECALLID" -K |
grep call.properties.number |
cut -d ':' -f 2 |
tr -d ' '
)"
}
loopincall() {
local CALLID
PICKUPORACCEPT="$1"
CALLID="$2"
GLOBAL_CALLID_INPROGRESS="$CALLID"
echo "Initiating call id $CALLID via $PICKUPORACCEPT with $(lookupnumberfromcallid $CALLID)"
mm -o "$CALLID" "$PICKUPORACCEPT"
# Audio routing setup
# Correct for some bug with modem requiring toggling DAI a few times to kick in
GLOBAL_AUDIOSAVE_FILE="$(mktemp)"
alsactl --file "$GLOBAL_AUDIOSAVE_FILE" store
for i in $(seq 3); do
pinephone-call-audio -e -m
pinephone-call-audio -e -m -2
done
# Loop to end
# CHECK FOR mmcli incall id here!
while true; do
OPTS="nop hangup volup voldown modespeaker modehandheld modewired 1 2 3 4 5 6 7 8 9 0"
[ -p /tmp/fbp.fifo ] && { echo -e "\b\f\r"; echo "$OPTS" | tr " " "\n"; } > /tmp/fbp.fifo
read -p "Action: " ACTION
[ "$ACTION" = "volup" ] && volset 1+
[ "$ACTION" = "voldown" ] && volset 1-
[ "$ACTION" = "modehandheld" ] && pinephone-call-audio -e -m -2
[ "$ACTION" = "modespeaker" ] && pinephone-call-audio -s -m -2
[ "$ACTION" = "modewired" ] && pinephone-call-audio -h -l -2
[ "$ACTION" = "hangup" ] && mm -o "$CALLID" --hangup && break
echo "$ACTION" | grep '[0-9]' && mm -o "$CALLID" --send-dtmf="$ACTION"
done
echo "Call terminated"
promptok
alsactl --file "$GLOBAL_AUDIOSAVE_FILE" restore && GLOBAL_AUDIOSAVE_FILE=""
}
phoneinfo() {
echo "phoneinfo: Diagnostics about the phone"
mm | grep -E 'Numbers|imei' | tr -s ' '
promptok
}
phonelog() {
eval "$F_PHONE_HOOKMISSEDSTOP" >&2 2>/dev/null &
FILTERGREP="."
while true; do
clear
touch "$F_PHONE_MODEMHISTORYFILE"
if [ "$FILTERGREP" = "." ]; then
cat "$F_PHONE_MODEMHISTORYFILE" | grep -E "$FILTERGREP"
else
cat "$F_PHONE_MODEMHISTORYFILE" | grep -F "$FILTERGREP"
fi
[ -p /tmp/fbp.fifo ] && {
echo -e "\b\f\r";
echo "cancel"
echo "."
cat "$F_PHONE_MODEMHISTORYFILE" | cut -f3 | sort | uniq
cat "$F_PHONE_MODEMHISTORYFILE" | cut -f1 | sort | uniq
} > /tmp/fbp.fifo
read -p "Filter: " FILTERGREP
[ "$FILTERGREP" == cancel ] && return
done
}
determinemodemtransition() {
local MONITORSTATE
if callid active >/dev/null; then
MONITORSTATE=incall
elif callid "incoming.+ringing" >/dev/null; then
MONITORSTATE=ringing
else
MONITORSTATE=idle
fi
[ "$(monitorstateget)" != "$MONITORSTATE" ] && {
TRANSITION="$(monitorstateget)_to_$MONITORSTATE"
monitorstateset "$MONITORSTATE"
echo "$TRANSITION"
}
}
devicepine64pinephone() {
amixer set "AIF1 DA0" "100%" > /dev/null
amixer set "AIF1 Slot 0 Digital DAC" unmute > /dev/null
GLOBAL_SPEAKER="Line Out"
GLOBAL_HEADPHONE="Headphone"
GLOBAL_EARPIECE="Earpiece"
}
syncmmstatetolog() {
for TEXTID in $(
mm --messaging-list-sms |
grep -Eo '/SMS/[0-9]+ \(received\)' |
grep -Eo '[0-9]+'
); do
TEXTDATA="$(mm -s "$TEXTID" -K)"
MSG="$(echo "$TEXTDATA" | grep sms.content.text | sed -E 's/^sms\.content\.text\s+:\s+//')"
NUM="$(cleannumber "$(echo "$TEXTDATA" | grep sms.content.number | sed -E 's/^sms\.content\.number\s+:\s+//')")"
TIME="$(echo "$TEXTDATA" | grep sms.properties.timestamp | sed -E 's/^sms\.properties\.timestamp\s+:\s+//' | xargs -IDATE date -d 'DATE' +%s)"
logevt textrecv "$(date +%s)" "$NUM" "$TIME" "$MSG"
mm --messaging-delete-sms="$TEXTID"
eval "$F_PHONE_HOOKMISSEDSTART" >&2 2>/dev/null &
done
for CALLID in $(
mm --voice-list-calls |
grep terminated |
grep -oE "Call\/[0-9]+" |
cut -d'/' -f2
); do
NUM="$(lookupnumberfromcallid "$CALLID")"
logevt callterminate "$(date +%s)" "$NUM"
mm --voice-delete-call "$CALLID"
done
}
monitorboot() {
local KILLPID EVTCALLADD EVTMSGADD EVTCALLPROP TRANSITION
KILLPID="$1"
EVTCALLADD="interface='org.freedesktop.ModemManager1.Modem.Voice',type='signal',member='CallAdded'"
EVTMSGADD="interface='org.freedesktop.ModemManager1.Modem.Messaging',type='signal',member='Added'"
EVTCALLPROP="interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',arg0='org.freedesktop.ModemManager1.Call'"
dbus-monitor --system "$EVTCALLADD" "$EVTMSGADD" "$EVTCALLPROP" | while read -r LINE; do
TRANSITION="$(determinemodemtransition)"
syncmmstatetolog >&2 2>/dev/null
if [ "$TRANSITION" = incall_to_idle ]; then
kill -HUP $KILLPID
elif [ "$TRANSITION" = idle_to_ringing ]; then
logevt callring "$(date +%s)" "$(lookupnumberfromcallid $(callid "incoming.+ringing"))"
eval "$F_PHONE_HOOKRINGSTART" >&2 2>/dev/null &
kill -HUP $KILLPID
elif [ "$TRANSITION" = ringing_to_idle ]; then
eval "$F_PHONE_HOOKRINGSTOP" >&2 2>/dev/null &
eval "$F_PHONE_HOOKMISSEDSTART" >&2 2>/dev/null &
kill -HUP $KILLPID
elif [ "$TRANSITION" = ringing_to_incall ]; then
eval "$F_PHONE_HOOKRINGSTOP" >&2 2>/dev/null &
fi
done
}
mainloop() {
while true; do
OPTS="maketext makecall phonelog phoneinfo modemrst"
clear
callid ringing >/dev/null && echo "Ringing in call from $(lookupnumberfromcallid $(callid "incoming.+ringing"))!" >&2
echo "Calls events: $(grep -c ^call $F_PHONE_MODEMHISTORYFILE)" >&2
echo "Text events: $(grep -c ^text $F_PHONE_MODEMHISTORYFILE)" >&2
echo "===================================" >&2
echo "phoneinfo: get sim card status, iccid, etc." >&2
echo "phonelog: read log for modem, dismisses unread messages/missed calls" >&2
echo "maketext: send a new text message" >&2
echo "makecall: place a new call" >&2
echo "modemrst: reset modemmanager and wait for connection"
if callid "incoming.+ringing" >/dev/null; then
echo "ringpickup: pickup the ringing in call" >&2
echo "ringhangup: hangup the ringing in call" >&2
OPTS="ringpickup ringhangup $OPTS"
fi
echo "===================================" >&2
[ -p /tmp/fbp.fifo ] && { echo -e "\b\f\r"; echo "$OPTS" | tr " " "\n"; } > /tmp/fbp.fifo
FUNC=none
while ! declare -F "$FUNC" > /dev/null; do
read -p "Run: " FUNC
done
clear
"$FUNC"
done
}
main() {
trap terminate INT TERM
trap mainloop HUP
GLOBAL_AUDIOSAVE_FILE=""
GLOBAL_CALLID_INPROGRESS=""
GLOBAL_SPEAKER=""
GLOBAL_HEADPHONE=""
GLOBAL_EARPIECE=""
installpolkitudevrules
ensuremmrunning
modemrst
env | grep -q "^$(basename "$0" | tr '[a-z]' '[A-Z]')=" || eval "$VAR"
eval "$(grep deviceinfo_codename /etc/deviceinfo | cut -d= -f2 | tr -d \"- | xargs -ID echo deviceD)" ||
errdie "Device unsupported - only pinephone supported currently"
monitorboot $$ >&2 2>/dev/null &
mainloop
}
if [ -n "$1" ]; then "$@"; else main; fi