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のおかげで凄い簡単に電話機能を実現できた。

関連記事:

ピザの店ベルペイ (滋賀県長浜市)

滋賀県長浜市に何度も通っているのだが、ピザの美味しい店があるということで行ってみた。

JR長浜駅から北側に道のりで2.4kmほど徒歩で30分程度ということで、歩いて行けない距離ではないが中途半端に遠い。

ピザの店ベルペイ 1
一つの敷地に数件の店が並んでいるような感じ。駐車場はそれなりに台数停められるようなのでクルマがあると来やすそうではある。
店の外観はおもいっきしイタリア国旗風。
PIZZAベルペイと書かれているのでメインはピザ、「ベルペイ」はアルファベットの綴りも意味も不明。
店の前に停まってる自転車はなんかいつもここにあるらしい(伝聞)。

ピザの店ベルペイ 2
店内の様子。昭和な喫茶店か夜の社交場的な謎の造りだが、奥にキッチンとカウンター席があってそこが店の2/3を占める。入り口を入ったところ左右ににテーブル席があるが10人分程度。
正午前に行ったけど既に満席だった。

ピザの店ベルペイ 3
牡蠣のピザ。牡蠣は大きくないがそれなりに数が入っている。これは旨味がとても強い。
サイズは大きくないので女性でも1皿くらいは楽にイケると思う。

ピザの店ベルペイ 4
海老のピザ。こちらも大きくない海老が使われているが、入っている数が多いのでそれなりに海老を食べた気になれる。

ピザの店ベルペイ 5
貝柱のピザ。貝柱はあまり目立たないのでバジルの抜けたマルゲリータみたいに見えるけどちゃんと貝柱(ホタテ?)が入っている。今回食べた中ではこれが一番好き。

ピザの店ベルペイ 6
海老のピザの断面。薄い生地はイタリアのピザの特徴。耳が薄くてパリッとしているのでローマ風っぽい。

個人的にはピザはワイズテーブルコーポレーションが展開するチェーン店のピッツァ・サルヴァトーレ・クオモ(PIZZA SALVATORE CUOMO)を基準としている。超有名というわけではないけど、日本中あちこちに店があるので話が通じやすいしテレビで派手にCM流してるようなクソ不味いのとはレベルが違う。だからサルヴァトーレ・クオモのピザと比べて劣らなければ少なくとも食べてがっかりすることはないといえる。
で、ベルペイはどうかっていうと、サルヴァトーレ・クオモのピザはナポリ風で生地を味わうものなので単純比較はできないけど、それに明らかに劣るということはない感じ。特に不満はなく十分に美味しい。
すっごい美味いかってなると都会には凄い店が幾つもあるしねぇ。名古屋側に近い場合を除いて滋賀県長浜市に行ける範囲の人が「ピザ食いてぇ」って思ったらまぁ選んで間違いないかなってくらい。

ベルペイのピザは1皿1000円程度なので昼ゴハンとかおやつとして気軽に利用できる範囲だと思うけど、注文してから焼くのに時間がかかるのと結構混んでることが多いみたいなので、時間に余裕があることが大事で店内で食べることにこだわらない方が良いかも。テイクアウトできるらしい。ナポリ風ピザだとテイクアウトすると生地がベチャッとなりやすいけどベルペイはローマ風っぽいのでベチャっにはならないかな?(根拠なし)

シェフの寺田寿正さんと寺田学さんはアサヒ緑健の緑効青汁の体験者としてCM?番組で店と共に紹介されていたらしい。たぶんテレビ東京とか衛星放送とかでやってるやつだと思う。緑効青汁とか興味がないので知らんけど。

記事中の画像はスマホでテキトーに撮影したものなのでせっかくの食べ物がなんかキチャナく撮れてるけどご容赦。

Up