Compare commits

...

19 Commits

Author SHA1 Message Date
klonfish 61ee978d54 Limit to only one simultaneous call 4 years ago
klonfish 6349cf0b1d Add handler for linhone errors 4 years ago
Fr3deric 61da307aa2 update documentation 4 years ago
Fr3deric 42bbf89a57 pass linphone config via `--factory-config` 4 years ago
Fr3deric f8d32c09a7 specify WorkingDirectory 4 years ago
Fr3deric d50a691252 Merge branch 'pylinphone' of https://git.blinkenbunt.org/LUG-Saar/fetapi into pylinphone 4 years ago
Fr3deric a05d547b1b add fetapd.service and some setup instructions 4 years ago
klonfish 52cff1474e Update function calls to pylinphone interface 4 years ago
klonfish 7e3d5a0b9d Update callbacks for pylinphone interface 4 years ago
klonfish d0b3fc4780 Fix account registration with linphone 4 years ago
klonfish 4f88ff760f Use linphone socket in sid chroot 4 years ago
klonfish 987359d1ad Update linphone imports 4 years ago
klonfish f3418971e6 Re-add callback registrations 4 years ago
Fr3deric 02fa43566e add pylinphone as submodule 4 years ago
Fr3deric cb4156ec5f add systemd service for linphone-daemon 4 years ago
Fr3deric a57c1e42a0 fix wrong variable name 4 years ago
Fr3deric 834233ac17 various Python 3 adjustments 4 years ago
klonfish c882bcd59f Merge pull request 'Replace linphone with pylinphone' (#1) from sqozz/fetapi:pylinphone into pylinphone 4 years ago
sqozz 579defbb7c Replace linphone with pylinphone 4 years ago
  1. 3
      .gitmodules
  2. 80
      README.md
  3. 10
      apparatinterface.py
  4. 10
      configreader.py
  5. 13
      fetapd.py
  6. 14
      fetapd.service
  7. 15
      linphone-daemon.service
  8. 3
      linphone.conf
  9. 158
      phoneinterface.py
  10. 1
      pylinphone
  11. 6
      statemachine.py

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "pylinphone"]
path = pylinphone
url = https://git.blinkenbunt.org/LUG-Saar/pylinphone.git

80
README.md

@ -1,7 +1,6 @@
# fetapi - FeTAp with a Raspberry Pi # fetapi - FeTAp with a Raspberry Pi
``` ```
,ooFeTApFeTApFeTA%=+-,. ,ooFeTApFeTApFeTA%=+-,.
,+FeTAp+' `o,, ,+FeTAp+' `o,,
,oFeTAp+ `, ,oFeTAp+ `,
@ -23,12 +22,33 @@
o:N H:a e, G o:N H:a e, G
t:a s:c c, ,x+x+' t:a s:c c, ,x+x+'
k:T:@:U:M:R:n; h, ,p+e+r+a+t/ k:T:@:U:M:R:n; h, ,p+e+r+a+t/
`@:n:M:E:' `@:n:M:E:' `T=i=s=c=h=A=p/
``` ```
## Installation ## Installation
Some of the following instructions must be executed with `root` privileges.
### Installing `fetapd`
1. Clone this repository (together with `pylinphone` as sub-module)
```
cd /opt
git clone https://git.blinkenbunt.org/LUG-Saar/fetapi.git
git submodule update
```
2. Configure and enable `fetapd` as `systemd` service
```
ln -sr fetapd.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable fetapd
```
### Installing `linphone-cli` on Raspbian ### Installing `linphone-cli` on Raspbian
1. Install `debootstrap` 1. Install `debootstrap`
@ -48,9 +68,65 @@
chroot debian-sid/ /debootstrap/debootstrap --second-stage chroot debian-sid/ /debootstrap/debootstrap --second-stage
``` ```
Note: Without `--variant=minbase`, sound won't work for some strange, unknown reason.
3. Install `linphone-cli` inside the chroot environment 3. Install `linphone-cli` inside the chroot environment
``` ```
chroot debian-sid/ apt -y install linphone-cli chroot debian-sid/ apt -y install linphone-cli
``` ```
4. Create user inside the chroot environment
```
chroot /var/tmp/debian-sid adduser --disabled-password pi
```
5. Bind-mount `/dev` to make ALSA accessible within the chroot
```
cat <<EOF >>/etc/fstab
/dev /var/tmp/debian-sid/dev none bind 0 0
EOF
mount -a
```
6. Copy *Linphone* configuration file
```
cp linphone.conf /var/tmp/debian-sid/home/pi/
```
7. Configure and enable `linphone-daemon` as `systemd` service
```
ln -sr linphone-daemon.service /etc/systemd/system
systemctl daemon-reload
systemctl enable linphone-daemon
```
## Configuring `fetapd`
:warning: TODO
### Identifying soundcard
```
# chroot /var/tmp/debian-sid /usr/bin/linphonec
...
linphonec> soundcard list
0: ALSA: default
1: ALSA: bcm2835 Headphones
2: ALSA: C-Media USB Headphone Set
```
## Starting `fetapd`
```
systemctl start fetapd
```
(or simply reboot your *FeTAp*)

10
apparatinterface.py

@ -66,14 +66,14 @@ class FeApUserInterface(object):
cb(self.__nsi_cnt % 10) cb(self.__nsi_cnt % 10)
def __on_nsi_falling(self, pin): def __on_nsi_falling(self, pin):
#print 'nsi' #print('nsi')
self.__nsi_cnt += 1 self.__nsi_cnt += 1
def __on_gabelschalter_change(self, pin): def __on_gabelschalter_change(self, pin):
gbstate = gpio.input(self.__pinconfig.pin_gabelschalter) gbstate = gpio.input(self.__pinconfig.pin_gabelschalter)
if self.__pinconfig.invert_gs: if self.__pinconfig.invert_gs:
gbstate = 1 - gbstate gbstate = 1 - gbstate
print 'gabelschalter:', gbstate print('gabelschalter:', gbstate)
for cb in self.__gabelschalter_callbacks: for cb in self.__gabelschalter_callbacks:
cb(gbstate) cb(gbstate)
@ -89,10 +89,10 @@ class FeApUserInterface(object):
gpio.output(self.__pinconfig.pin_wecker_b, 1) gpio.output(self.__pinconfig.pin_wecker_b, 1)
time.sleep(0.02) time.sleep(0.02)
c += 40 c += 40
print 'ring' print('ring')
gpio.output(self.__pinconfig.pin_wecker_enable, 0) gpio.output(self.__pinconfig.pin_wecker_enable, 0)
print '' print('')
time.sleep(4) time.sleep(4)
@ -123,7 +123,7 @@ if __name__ == '__main__':
t = FeApUserInterface(pinconfig) t = FeApUserInterface(pinconfig)
def dailed(num): def dailed(num):
print num print(num)
t.add_nummernschalter_callback(dailed) t.add_nummernschalter_callback(dailed)

10
configreader.py

@ -1,5 +1,5 @@
import csv import csv
import ConfigParser import configparser
import apparatinterface import apparatinterface
import phoneinterface import phoneinterface
import statemachine import statemachine
@ -20,7 +20,7 @@ class ConfigurationReader(object):
} }
def __init__(self): def __init__(self):
self.__cp = ConfigParser.ConfigParser(defaults=ConfigurationReader.DEFAULTS) self.__cp = configparser.ConfigParser(defaults=ConfigurationReader.DEFAULTS)
self.pinconfig = None self.pinconfig = None
self.dialconfig = None self.dialconfig = None
self.phoneconfig = None self.phoneconfig = None
@ -45,9 +45,9 @@ class ConfigurationReader(object):
shortcuts = {} shortcuts = {}
with open(fname, 'r') as csvfile: with open(fname, 'r') as csvfile:
for row in csv.DictReader(csvfile): for row in csv.DictReader(csvfile):
print 'row', row print('row', row)
shortcuts[row['shortcut']] = row['number'] shortcuts[row['shortcut']] = row['number']
print 'shortcuts:', shortcuts print('shortcuts:', shortcuts)
return shortcuts return shortcuts
def __read_blacklist(self): def __read_blacklist(self):
@ -61,7 +61,7 @@ class ConfigurationReader(object):
def read(self, f): def read(self, f):
self.__cp.read(f) self.__cp.read(f)
print 'pin_nsa:', self.__get_global_val_int('pin_nsa'), print('pin_nsa:', self.__get_global_val_int('pin_nsa'),)
self.pinconfig = apparatinterface.FeApPinConfiguration( self.pinconfig = apparatinterface.FeApPinConfiguration(
gpio_numbering = self.__get_global_val('gpio_numbering'), gpio_numbering = self.__get_global_val('gpio_numbering'),
pin_nsa = self.__get_global_val_int('pin_nsa'), pin_nsa = self.__get_global_val_int('pin_nsa'),

13
fetapd.py

@ -65,7 +65,7 @@ def phone_cb(event):
controller.queue_event('invalid_number') controller.queue_event('invalid_number')
if __name__ == '__main__': if __name__ == '__main__':
print FeTAp print(FeTAp)
cfg = configreader.ConfigurationReader() cfg = configreader.ConfigurationReader()
cfg.read('fetap.ini') cfg.read('fetap.ini')
@ -74,6 +74,15 @@ if __name__ == '__main__':
feap = FeApUserInterface(cfg.pinconfig) feap = FeApUserInterface(cfg.pinconfig)
controller = statemachine.StateMachineController(phone, feap, cfg.dialconfig) controller = statemachine.StateMachineController(phone, feap, cfg.dialconfig)
# TODO: Use real events from daemon
controller.queue_event('registration_in_progress')
controller.queue_event('registration_successful')
feap.add_gabelschalter_callback(gabelschalter_cb)
feap.add_nummernschalter_active_callback(nummernschalter_active_cb)
feap.add_nummernschalter_done_callback(nummernschalter_done_cb)
phone.add_event_cb(phone_cb)
phone.start() phone.start()
try: try:
while True: while True:
@ -82,5 +91,5 @@ if __name__ == '__main__':
except KeyboardInterrupt: except KeyboardInterrupt:
phone.stop() phone.stop()
feap.set_wecker(False) feap.set_wecker(False)
c.stop() controller.stop()

14
fetapd.service

@ -0,0 +1,14 @@
[Unit]
Description=FeTAp Daemon
Requires=linphone-daemon.service
After=linphone-daemon.service
[Service]
Type=simple
WorkingDirectory=/opt/fetapi
ExecStart=/usr/bin/python3 /opt/fetapi/fetapd.py
User=pi
Restart=on-failure
[Install]
WantedBy=multi-user.target

15
linphone-daemon.service

@ -0,0 +1,15 @@
[Unit]
Description=Linphone Daemon
Requires=var-tmp-debian\x2dsid-dev.mount
After=var-tmp-debian\x2dsid-dev.mount
[Service]
Type=simple
RootDirectory=/var/tmp/debian-sid
ExecStartPre=/bin/rm -f /tmp/linphone
ExecStart=/usr/bin/linphone-daemon --pipe linphone --factory-config /home/pi/linphone.conf
User=pi
Restart=on-failure
[Install]
WantedBy=multi-user.target

3
linphone.conf

@ -17,3 +17,6 @@ firewall_policy=2
mtu=0 mtu=0
download_bw=0 download_bw=0
upload_bw=0 upload_bw=0
[misc]
max_calls=1

158
phoneinterface.py

@ -1,7 +1,7 @@
import linphone
import time import time
import threading import threading
import subprocess import subprocess
from pylinphone.pylinphone import LinphoneCommunicationSocket
class PhoneProxyConfiguration(object): class PhoneProxyConfiguration(object):
@ -40,46 +40,27 @@ class PhoneEvent(object):
@classmethod @classmethod
def string(cls, val): def string(cls, val):
for k, v in vars(cls).iteritems(): for k, v in vars(cls).items():
if v == val: if v == val:
return k return k
class PhoneInterface(object): class PhoneInterface(object):
def __init__(self, config): def __init__(self, config):
cbs = {
'global_state_changed': self.__global_state_changed,
'registration_state_changed': self.__registration_state_changed,
'call_state_changed': self.__call_state_changed
}
self.__event_cbs = [] self.__event_cbs = []
self.__config = config self.__config = config
self.__core = linphone.Core.new(cbs, None, config.linphone_config) self.__core = LinphoneCommunicationSocket("/var/tmp/debian-sid/tmp/linphone")
self.__core.onLinphoneCallIncomingReceived = self.on_LinphoneCallIncomingReceived
self.__core.onLinphoneCallOutgoingRinging = self.on_LinphoneCallOutgoingRinging
self.__core.onLinphoneCallConnected = self.on_LinphoneCallConnected
self.__core.onLinphoneCallEnd = self.on_LinphoneCallEnd
self.__core.onLinphoneCallError = self.on_LinphoneCallError
# Create and add all proxy configs # Create and add all proxy configs
for p in config.proxies: for p in config.proxies:
ainfo = self.__core.create_auth_info(p.username, p.username, aid = self.__core.register(p.identity, p.proxy, p.password, p.username) # sip:XXXX@hg.eventphone.de, hg.eventphone.de, MySecretPassword, XXXX
p.password, None, p.realm,
None)
self.__core.add_auth_info(ainfo)
pconf = self.__core.create_proxy_config()
pconf.edit()
if self.__core.version < '3.9.0':
pconf.identity = p.identity
else:
pconf.identity_address = pconf.normalize_sip_uri(p.identity)
pconf.publish_enabled = False
pconf.realm = p.realm
pconf.register_enabled = True
pconf.server_addr = p.proxy
self.__core.add_proxy_config(pconf)
pconf.done()
if p.name == config.default_proxy:
self.__core.default_proxy_config = pconf
self.__audioproc = None self.__audioproc = None
aplay = subprocess.Popen(['aplay', '-qD%s' % config.sound_device], aplay = subprocess.Popen(['aplay', '-qD%s' % config.sound_device],
@ -89,68 +70,44 @@ class PhoneInterface(object):
stdout=aplay.stdin) stdout=aplay.stdin)
# Set default parameters overriding the ones from the given config file # Set default parameters overriding the ones from the given config file
self.__core.set_user_agent('FeTAp 615', '0.1') # TODO: figure out how to set at least some of these settings through the unix socket
self.__core.stun_server = config.stun_server #self.__core.set_user_agent('FeTAp 615', '0.1')
self.__core.ringback = '' #self.__core.stun_server = config.stun_server
self.__core.max_calls = 1 #self.__core.ringback = ''
self.__core.inc_timeout = config.incoming_timeout #self.__core.max_calls = 1
self.__core.set_call_error_tone(linphone.Reason.Busy, '') #self.__core.inc_timeout = config.incoming_timeout
self.__core.disable_chat(linphone.Reason.None) #self.__core.set_call_error_tone(linphone.Reason.Busy, '')
self.__core.echo_cancellation_enabled = False #self.__core.disable_chat(linphone.Reason.None)
self.__core.video_capture_enabled = False #self.__core.echo_cancellation_enabled = False
self.__core.video_display_enabled = False #self.__core.video_capture_enabled = False
#self.__core.video_display_enabled = False
def __global_state_changed(self, core, state, msg):
print 'Global state changed:', state, msg
# TODO: Do we need events emitted here? def run_callbacks(self, evt):
pass print(PhoneEvent.string(evt))
def __registration_state_changed(self, core, proxyconf, state, msg):
print 'Registration state changed:', proxyconf, state, msg
evt = None
if state == linphone.RegistrationState.Progress:
evt = PhoneEvent.RegInProgress
elif state == linphone.RegistrationState.Ok:
evt = PhoneEvent.RegSuccessfull
elif state == linphone.RegistrationState.None:
evt = PhoneEvent.RegLost
if evt is not None:
for cb in self.__event_cbs: for cb in self.__event_cbs:
cb(evt) cb(evt)
else:
print 'Unhandled registration state:', linphone.RegistrationState.string(state) def on_LinphoneCallIncomingReceived(self, event):
self.run_callbacks(PhoneEvent.CallIncoming)
def __call_state_changed(self, core, call, state, msg):
print 'Call state changed:', call, state, msg def on_LinphoneCallOutgoingRinging(self, event):
evt = None self.run_callbacks(PhoneEvent.CallRinging)
if state == linphone.CallState.IncomingReceived:
evt = PhoneEvent.CallIncoming def on_LinphoneCallConnected(self, event):
elif state == linphone.CallState.OutgoingRinging: self.run_callbacks(PhoneEvent.CallAccepted)
evt = PhoneEvent.CallRinging
elif state == linphone.CallState.Connected: def on_LinphoneCallEnd(self, event):
evt = PhoneEvent.CallAccepted self.run_callbacks(PhoneEvent.CallEnded)
elif state == linphone.CallState.End:
evt = PhoneEvent.CallEnded def on_LinphoneCallError(self, event):
elif state == linphone.CallState.Error: # TODO: Distinguish between different errors
error = call.error_info.reason self.run_callbacks(PhoneEvent.CallBusy)
if error == linphone.Reason.Busy:
evt = PhoneEvent.CallBusy
elif error == linphone.Reason.NotFound:
evt = PhoneEvent.CallInvalidNumber
else:
evt = PhoneEvent.CallEnded
if evt is not None:
for cb in self.__event_cbs:
cb(evt)
else:
print 'Unhandled call state:', linphone.CallState.string(state)
def __pollthread(self): def __pollthread(self):
while self.__running: while self.__running:
self.__core.iterate() self.__core.process_event()
time.sleep(0.02) # Value from example code time.sleep(0.2) # Value for good measure
def start(self): def start(self):
self.__running = True self.__running = True
@ -167,23 +124,32 @@ class PhoneInterface(object):
self.__event_cbs.append(cb) self.__event_cbs.append(cb)
def call(self, number): def call(self, number):
if '@' not in number and self.__core.default_proxy_config is None: if '@' not in number:
proxy = None
default_name = self.__config.default_proxy
for p in self.__config.proxies:
if p.name == default_name:
proxy = p
break
if proxy is None:
# Try to resolve prefix # Try to resolve prefix
for p in self.__config.proxies: for p in self.__config.proxies:
if number.startswith(p.prefix): if number.startswith(p.prefix):
number = number[len(p.prefix):] number = number[len(p.prefix):]
number += '@' + p.realm proxy = p
break break
self.__core.invite(number) if proxy is not None:
number += '@' + proxy.realm
self.__core.call(number)
def accept_call(self): def accept_call(self):
self.__core.accept_call(self.__core.current_call) self.__core.answer()
def decline_call(self): def decline_call(self):
self.__core.decline_call(self.__core.current_call, linphone.Reason.Busy) self.__core.decline_call(self.__core.current_call)
def end_call(self): def end_call(self):
self.__core.terminate_call(self.__core.current_call) self.__core.terminate()
def play_dial_tone(self): def play_dial_tone(self):
self.stop_playing() self.stop_playing()
@ -213,15 +179,17 @@ class PhoneInterface(object):
self.__audioproc.terminate() self.__audioproc.terminate()
def read_text(self, text): def read_text(self, text):
self.__ttsproc.stdin.write(text + '\n') self.__ttsproc.stdin.write(text.encode('utf8') + b'\n')
self.__ttsproc.stdin.flush() self.__ttsproc.stdin.flush()
def get_remote_number(self): def get_remote_number(self):
return self.__core.current_call_remote_address.username # FIXME
#return self.__core.current_call_remote_address.username
return '0000'
if __name__ == '__main__': if __name__ == '__main__':
def event_cb(evt): def event_cb(evt):
print 'Got event:', PhoneEvent.string(evt) print('Got event:', PhoneEvent.string(evt))
try: try:
phone = PhoneInterface('.linphonerc-foo', '.linphonerc') phone = PhoneInterface('.linphonerc-foo', '.linphonerc')

1
pylinphone

@ -0,0 +1 @@
Subproject commit beb544b3b614ae824ddd2d571dda617c61082c92

6
statemachine.py

@ -1,5 +1,5 @@
import threading import threading
import Queue as queue import queue
class DialConfiguration(object): class DialConfiguration(object):
@ -263,10 +263,10 @@ class DialingState(BaseState):
def on_timeout(self): def on_timeout(self):
number = self.__number number = self.__number
print 'Dialing number:', number print('Dialing number:', number)
if number in self._controller.dialconfig.shortcuts: if number in self._controller.dialconfig.shortcuts:
number = self._controller.dialconfig.shortcuts[number] number = self._controller.dialconfig.shortcuts[number]
print 'shortcut resolved:', number print('shortcut resolved:', number)
self._controller.phone.call(number) self._controller.phone.call(number)
return ConnectingState return ConnectingState

Loading…
Cancel
Save