Compare commits

..

No commits in common. 'pylinphone' and 'master' have entirely different histories.

  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. 168
      phoneinterface.py
  10. 1
      pylinphone
  11. 6
      statemachine.py

3
.gitmodules vendored

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

80
README.md

@ -1,6 +1,7 @@
# fetapi - FeTAp with a Raspberry Pi # fetapi - FeTAp with a Raspberry Pi
``` ```
,ooFeTApFeTApFeTA%=+-,. ,ooFeTApFeTApFeTA%=+-,.
,+FeTAp+' `o,, ,+FeTAp+' `o,,
,oFeTAp+ `, ,oFeTAp+ `,
@ -22,33 +23,12 @@
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:' `T=i=s=c=h=A=p/ `@:n:M:E:'
``` ```
## 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`
@ -68,65 +48,9 @@ Some of the following instructions must be executed with `root` privileges.
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,15 +74,6 @@ 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:
@ -91,5 +82,5 @@ if __name__ == '__main__':
except KeyboardInterrupt: except KeyboardInterrupt:
phone.stop() phone.stop()
feap.set_wecker(False) feap.set_wecker(False)
controller.stop() c.stop()

14
fetapd.service

@ -1,14 +0,0 @@
[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

@ -1,15 +0,0 @@
[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,6 +17,3 @@ firewall_policy=2
mtu=0 mtu=0
download_bw=0 download_bw=0
upload_bw=0 upload_bw=0
[misc]
max_calls=1

168
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,27 +40,46 @@ class PhoneEvent(object):
@classmethod @classmethod
def string(cls, val): def string(cls, val):
for k, v in vars(cls).items(): for k, v in vars(cls).iteritems():
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 = LinphoneCommunicationSocket("/var/tmp/debian-sid/tmp/linphone") self.__core = linphone.Core.new(cbs, None, config.linphone_config)
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:
aid = self.__core.register(p.identity, p.proxy, p.password, p.username) # sip:XXXX@hg.eventphone.de, hg.eventphone.de, MySecretPassword, XXXX ainfo = self.__core.create_auth_info(p.username, p.username,
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],
@ -70,44 +89,68 @@ 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
# TODO: figure out how to set at least some of these settings through the unix socket self.__core.set_user_agent('FeTAp 615', '0.1')
#self.__core.set_user_agent('FeTAp 615', '0.1') self.__core.stun_server = config.stun_server
#self.__core.stun_server = config.stun_server self.__core.ringback = ''
#self.__core.ringback = '' self.__core.max_calls = 1
#self.__core.max_calls = 1 self.__core.inc_timeout = config.incoming_timeout
#self.__core.inc_timeout = config.incoming_timeout self.__core.set_call_error_tone(linphone.Reason.Busy, '')
#self.__core.set_call_error_tone(linphone.Reason.Busy, '') self.__core.disable_chat(linphone.Reason.None)
#self.__core.disable_chat(linphone.Reason.None) self.__core.echo_cancellation_enabled = False
#self.__core.echo_cancellation_enabled = False self.__core.video_capture_enabled = False
#self.__core.video_capture_enabled = False self.__core.video_display_enabled = False
#self.__core.video_display_enabled = False
def __global_state_changed(self, core, state, msg):
print 'Global state changed:', state, msg
def run_callbacks(self, evt): # TODO: Do we need events emitted here?
print(PhoneEvent.string(evt)) pass
for cb in self.__event_cbs:
cb(evt) def __registration_state_changed(self, core, proxyconf, state, msg):
print 'Registration state changed:', proxyconf, state, msg
def on_LinphoneCallIncomingReceived(self, event): evt = None
self.run_callbacks(PhoneEvent.CallIncoming) if state == linphone.RegistrationState.Progress:
evt = PhoneEvent.RegInProgress
def on_LinphoneCallOutgoingRinging(self, event): elif state == linphone.RegistrationState.Ok:
self.run_callbacks(PhoneEvent.CallRinging) evt = PhoneEvent.RegSuccessfull
elif state == linphone.RegistrationState.None:
def on_LinphoneCallConnected(self, event): evt = PhoneEvent.RegLost
self.run_callbacks(PhoneEvent.CallAccepted)
if evt is not None:
def on_LinphoneCallEnd(self, event): for cb in self.__event_cbs:
self.run_callbacks(PhoneEvent.CallEnded) cb(evt)
else:
def on_LinphoneCallError(self, event): print 'Unhandled registration state:', linphone.RegistrationState.string(state)
# TODO: Distinguish between different errors
self.run_callbacks(PhoneEvent.CallBusy) def __call_state_changed(self, core, call, state, msg):
print 'Call state changed:', call, state, msg
evt = None
if state == linphone.CallState.IncomingReceived:
evt = PhoneEvent.CallIncoming
elif state == linphone.CallState.OutgoingRinging:
evt = PhoneEvent.CallRinging
elif state == linphone.CallState.Connected:
evt = PhoneEvent.CallAccepted
elif state == linphone.CallState.End:
evt = PhoneEvent.CallEnded
elif state == linphone.CallState.Error:
error = call.error_info.reason
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.process_event() self.__core.iterate()
time.sleep(0.2) # Value for good measure time.sleep(0.02) # Value from example code
def start(self): def start(self):
self.__running = True self.__running = True
@ -124,32 +167,23 @@ 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: if '@' not in number and self.__core.default_proxy_config is None:
proxy = None # Try to resolve prefix
default_name = self.__config.default_proxy
for p in self.__config.proxies: for p in self.__config.proxies:
if p.name == default_name: if number.startswith(p.prefix):
proxy = p number = number[len(p.prefix):]
number += '@' + p.realm
break break
if proxy is None: self.__core.invite(number)
# Try to resolve prefix
for p in self.__config.proxies:
if number.startswith(p.prefix):
number = number[len(p.prefix):]
proxy = p
break
if proxy is not None:
number += '@' + proxy.realm
self.__core.call(number)
def accept_call(self): def accept_call(self):
self.__core.answer() self.__core.accept_call(self.__core.current_call)
def decline_call(self): def decline_call(self):
self.__core.decline_call(self.__core.current_call) self.__core.decline_call(self.__core.current_call, linphone.Reason.Busy)
def end_call(self): def end_call(self):
self.__core.terminate() self.__core.terminate_call(self.__core.current_call)
def play_dial_tone(self): def play_dial_tone(self):
self.stop_playing() self.stop_playing()
@ -179,17 +213,15 @@ class PhoneInterface(object):
self.__audioproc.terminate() self.__audioproc.terminate()
def read_text(self, text): def read_text(self, text):
self.__ttsproc.stdin.write(text.encode('utf8') + b'\n') self.__ttsproc.stdin.write(text + '\n')
self.__ttsproc.stdin.flush() self.__ttsproc.stdin.flush()
def get_remote_number(self): def get_remote_number(self):
# FIXME return self.__core.current_call_remote_address.username
#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

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

6
statemachine.py

@ -1,5 +1,5 @@
import threading import threading
import queue import Queue as 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