NanoPi NEOをSIP電話機にする 後編 (その2)

NanoPi NEOをSIP電話機にする

NanoPi NEOをSIP電話機にする 後編 (その1)の後に続きの記事をすぐに出したつもりですっかり忘れ果てていた。
ということで、いまさらだけど続き。

中編のLinphone(linphonec)が既に動く状態であるとする。
今回はそのLinphoneに付属するLinphonecshを使う。

linphonecsh

$ linphonecsh
Usage:
linphonecsh  [arguments]
where action is one of
  init        : spawn a linphonec daemon (first step to make other actions)
              followed by the arguments sent to linphonec
  generic     : sends a generic command to the running linphonec daemon
              followed by the generic command surrounded by quotes,
               for example "call sip:joe@example.net"
  register    : register; arguments are 
              --host 
              --username 
              --password 
  unregister  : unregister
  dial        : dial 
  status      : can be 'status register', 'status autoanswer' or 'status hook'
  soundcard   : can be 'soundcard capture', 'soundcard playback', 'soundcard ring',
               followed by an optional number representing the index of the soundcard,
               in which case the soundcard is set instead of just read.
  exit        : make the linphonec daemon to exit.

利用できるコマンドは多くない。ただし、genericが使える。linphonecのコマンドをgenericの後に指定することでlinphonecでできることの多くが実行できる。
最初にlinphonecsh initを実行するとlinphonecがバックグラウンド(デーモン化?)で動くみたい。linphonecsh exitでバックグラウンドのlinphonecが終了。

$ ps -ax | grep linphonecsh
 9876 ?        Ssl    0:19 linphonec --pipe -c /dev/null
他プロセス省略
linphonecsh init後のプロセスリスト

サウンド周りの確認

NanoPi NEO2に繋いだUSBハンドセットのスピーカーで音を出す、そのマイクで音を拾う。

# aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: Codec [H3 Audio Codec], device 0: CDC PCM Codec-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: U0x4b40x307 [USB Device 0x4b4:0x307], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

USBのハンドセットはUSB Device 0x4b4:0x307と認識されている。重要なのは赤字の部分。これがデバイスIDで後で使う。この例ではコロンの有無しか違わないけどこれを間違うと思い通りに動かない。

引き続きミキサーでコントロール対象のポート名を見る。ここでamixerを使うとわからなくなってしまうのでalsamixerを使う方が無難。

# alsamixer

alsamixerを確認 1
alsamixerが起動したときの画面。標準のサウンドデバイスの状態が表示されている筈。
上の画像だとNanoPi NEO2の標準 H3 Audio Codecになっている(画像左上の赤色の破線の四角部分)。つまりNanoPiのオーディオ用ピンヘッダ(購入時未取り付け)にスピーカーを繋いだときに鳴るやつ。
今回はUSBハンドセットを確認するので右上の赤い四角部分、ファンクションキー6番[F6]で表示するサウンドカード(デバイス)を変更する。

alsamixerを確認 2
サウンドカード(デバイス)のリストが画面中央に表示されるので矢印キー上下でUSBハンドセットのデバイスに合わせて[Enter]。上の画像ではUSB Device 0x4b4:0x307を選んだ。

alsamixerを確認 3
画像の左上、カード(デバイス)はUSBデバイスになっている。ポート(プロファイル)はおそらく再生用(Playback)だけが表示されている筈なので録音(Capture)も合わせて確認する。[F5]を押すと両方が表示される。
上の画像では再生用はPCM、録音用はMicとなっている。

USBハンドセットで電話するスクリプト

後編 (その1)で使用したスクリプトにちょこっと足しただけ。ブログ記事なので最低限動く程度の簡易版とする。細かいことはしない。
また、エラー処理もかなり手抜き。ただし、ログだけは取るようにした。
本当はSIPサーバへのレジスト状態を確認して必要に応じて再レジストするとかレジストできない場合のエラー処理を入れたかったが、NanoPi NEO2用にインストールしたlinphonecshが悪いのか他の何かが原因なのかlinphonecsh status registerでレジスト状態に関わらず-1を返されることが多いので今回は解決せずに無視することにした。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#!/usr/bin/python

import re
import time
from datetime import datetime
import subprocess
import struct
from PIL import Image, ImageDraw, ImageFont

keys = {
  0x00 : "",
  0x03 : "1",
  0x09 : "2",
  0x0f : "3",
  0x04 : "4",
  0x0a : "5",
  0x10 : "6",
  0x05 : "7",
  0x0b : "8",
  0x11 : "9",
  0x06 : "*",
  0x0c : "0",
  0x12 : "#",
  0x02 : "left",
  0x0e : "right",
  0x01 : "yes",
  0x0d : "no",
  0x19 : "vol+",
  0x1c : "vol-",
  0x1b : "mute",
}

hiddev = "/dev/hidraw0"   #ハンドセットのデバイス
sndcard = "U0x4b40x307"   #aplay -lで表示されるデバイスID
snddev = "PCM"            #再生用ポート名
micdev = "Mic"            #マイク用ポート名
modmute = False           #通話時ミュート初期値(オフ)
spkrvol = 60              #通話時のスピーカー音量(60%)可変
micvol = 50               #通話時のマイク音量(50%)固定
cfrom = ""                #通話相手の表示用(空白)


def func_aplog(str):
  f = open('./phone.log', 'a')
  f.write(datetime.now().strftime("%Y/%m/%d %H:%M:%S ") + str)
  f.close()

def func_linphone(cat, cmd, bln):
  try:
    if bln == 1:
      ret = subprocess.check_output(cmd.split())
    else:
      ret = subprocess.check_output(cmd)

    func_aplog(cat + ": " + ret + "\n")
    return ret
  except subprocess.CalledProcessError as err:
    func_aplog(cat + " ERROR:\n")
    func_aplog(str(err.returncode) + "\n")
    func_aplog(str(err.cmd) + "\n")
    func_aplog(err.output + "\n")
    return "err"

def func_lgtonoff(bln):
  file = open( hiddev, "w+b" );
  if bln:
    buf = "040f".decode("hex")
  else:
    buf = "0400".decode("hex")
  file.write(buf)
  file.flush()
  file.close();

cmd = '/usr/bin/linphonecsh init'
func_linphone("INIT", cmd, True)
time.sleep(1)

cmd = '/usr/bin/linphonecsh register --username 6000 --host 192.168.0.100 --password himitsu'
func_linphone("REGIST", cmd, True)
time.sleep(1)

cmd = ['/usr/bin/linphonecsh', 'generic', 'soundcard use 2']
print(func_linphone("STATUS", cmd, False))

subprocess.check_output(['amixer', '-c', sndcard, 'set', snddev, '100%'])
subprocess.check_output(['amixer', '-c', sndcard, 'set', micdev, str(micvol) + '%'])

file = open( hiddev, "w+b" );

def getKey():
  buf = file.read(8)
  return keys[ord(buf[1])]

def paint():
  pixels = img.load() # create the pixel map
  pixels = img.transpose(Image.ROTATE_180).load() # create the pixel map
  b = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  for r in range(0, 8):
    for s in range (0, 12):
      for i in range (0, 12):
        b[i] = 0
        for j in range (0, 8):
          if (pixels[i + 12 * s, j + 8 * r] < 128):
            b[i] |= 2**j
      buf="0301".decode("hex")+chr(r)+chr(s*11)+chr(b[0])+chr(b[1])+chr(b[2])+chr(b[3])+chr(b[4])+chr(b[5])+chr(b[6])+chr(b[7])+chr(b[8])+chr(b[9])+chr(b[10])+chr(b[11])
      file.write(buf)
      file.flush()

def drawText(text):
  draw.text((0, 0), drawText.old , 255, font=font)
  draw.text((0, 0), text , 0, font=font)
  drawText.old = text
  paint()

drawText.old = ""
img = Image.new( '1', (144,64), "white")
draw = ImageDraw.Draw(img)

font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 20)
text = ""

# write canvas to display
paint()

# clear canvas
draw.rectangle((0,0,142,63), fill=255)

while( 1 ):
  k = getKey();
  if (k == ""):
    0
  elif (k == "yes"):
    reslt = subprocess.check_output(['/usr/bin/linphonecsh', 'generic', 'calls'])
    if 'No active call.' not in reslt:
      cfrom = re.findall("[\w.-]+@[\w.-]+.\w+", reslt)
      drawText(cfrom[0])
  elif (k == "no"):
    text = ""
    drawText(text)
  elif (k == "left"):
    func_lgtonoff(True)
    reslt = subprocess.check_output(['/usr/bin/linphonecsh', 'generic', 'calls'])
    if 'IncomingReceived' in reslt:
      text = ""
      subprocess.check_output(['amixer', '-c', sndcard, 'set', snddev, '50%'])
      cmd = ["/usr/bin/linphonecsh", "generic", "answer"]
      func_linphone("ANSWER", cmd, False)
      drawText("Answer")
    elif 'No active call.' in reslt:
      if len(text) > 2:
        cmd = ['/usr/bin/linphonecsh', 'dial', text]
        drawText('DIAL' + text)
        func_linphone("DIAL", cmd, False)
        text = ""
  elif (k == "right"):
    text = "Terminate"
    drawText(text)
    cmd = ['/usr/bin/linphonecsh', 'generic', 'terminate']
    func_linphone("STATUS", cmd, False)
    time.sleep(1)
    text = ""
    drawText(text)
    func_lgtonoff(False)
    subprocess.check_output(['amixer', '-c', sndcard, 'set', snddev, '100%'])
  elif (k == "vol+"):
    spkrvol += 10
    if spkrvol > 100:
      spkrvol = 100
    subprocess.check_output(['amixer', '-c', sndcard, 'set', snddev, str(spkrvol) + '%'])
    drawText("Vol:" + str(spkrvol) + "%")
  elif (k == "vol-"):
    spkrvol -= 10
    if spkrvol < 10:
      spkrvol = 10
    subprocess.check_output(['amixer', '-c', sndcard, 'set', snddev, 0])
    drawText("Vol:" + str(spkrvol) + "%")
  elif (k == "mute"):
    if modmute: 
        cmd = ['/usr/bin/linphonecsh', 'generic', 'unmute']
        func_linphone("MUTE_OFF", cmd, False)
        modmute = False
        drawText("Mute Off")
    else:
        cmd = ['/usr/bin/linphonecsh', 'generic', 'mute']
        func_linphone("MUTE_ON", cmd, False)
        modmute = True
        drawText("Mute On")
  else:
    text = text + k
    drawText(text)

file.close();
SIPサーバへのレジストは78行目。

USBハンドセットボタン
①はスクリプトでleftのボタン。RING鳴動時の着信用と宛先番号入力後の発信用として使う。
④はスクリプトでrightのボタン。通話終了やダイヤル入力のリセットに使う。
⑤はスクリプトでyesのボタン。通話中に相手の番号をハンドセットの液晶に表示する。
⑦はスクリプトでnoのボタン。液晶の文字を消す(だけ)。
②③⑥は使わない。
Vol+は通話時のスピーカーの音量を10%上げる。(最大100%)
Vol+は通話時のスピーカーの音量を10%下げる。(最小10%)
muteは通話時にハンドセットのマイクをオフにする。もう一度押すとオン。(トグル)

液晶のバックライトは①(left)を押すと点灯。③(right)を押すと1秒後に消灯、ただそれだけ。相手が通話を終了させた場合でも手動で③(right)を押さないとバックライトは消灯しない。
また、発進時に相手の番号を入力してから①(left)を押すと番号入力時はバックライト消灯なので、①(left)を押して番号入力して再度①(left)を行うとバックイト点灯で番号を入力できる。番号入力中に放置してもずっとその状態なので③(right)でリセットする必要がある。

手抜きなので携帯などとは少し勝手が違うけど必要最小限のことはできる。着信時のRINGは音量100%固定のつもり。マイクの音量も50%で固定。上のスクリプトではボタン操作では変更できないのでスクリプトを変更して調整。

たぶん、こんなん気に入らねぇと思うだろうから好みに改造すれば良いかと。

Pythonは殆ど使ったことがないので文法間違ってたらスンマセン。

後編 (その1)と今回利用したCheap USB Skype/VoIP phone protocol discoveryのスクリプトの作者に感謝。これとlinphonecshのおかげで凄い簡単に電話機能を実現できた。

関連記事: