目次
背景
唐突に黒電話をIP電話化したくなったのですが、それだけであれば既にやっている方が大勢いらっしゃいます。 何か面白いことはないか検討した結果、ナンバーディスプレイを設置するのがいいということになりましたので どのように実現したのかを書きたいと思います。
構成
そもそもどのような構成になっているのかということを初めに図で示します。
おおよそどのような仕組みで動いているのかは理解して頂けたのではと思います。
使ったもの
- RaspberryPi3
- VoIPルーター YAMAHA NVR500
- I2C LCD Display
- 黒電話(電電公社のやつ)
私は電電公社のものを使用したのでコード変換に手間取りましたがこちらのような商品を購入すれば、 初めから電話のコネクタがRJ-11なので幸せになれるのではないでしょうか?知らんけど
SIPクライアントの設定について
黒電話をIP電話化するにあたって YAMAHA NVR500 をSIPクライアントとして活用しているのですが、 このあたりのやり方は他のブログをあたって頂いた方が分かりやすく記載されていると思いますので、 当ブログとしては説明を省略します。
ちなみに、050の電話番号は Smartalk のものを使用しました。今でも電話番号を取得できる合法的な裏技がありますので 検索してみて下さい。
ラズベリーパイでの制作
今回のナンバーディスプレイはラズベリーパイを用いて実現しました。
配線
適当につなぎます。
ディスプレイのコントロール
ディスプレイのコントロールには以下のようなプログラムを用意しました。 OSOYOOという会社の提供するソースコードを自分用に書き換えたような気が致します。
LcdController.py
import smbus
import time
import threading
# 通話時間および電話番号の表示を行うクラス
class TalkTimer(threading.Thread):
# コンストラクタ
def __init__(self):
# 継承
super().__init__()
# 一行目保持用
self.line1 = ""
# マルチスレッド用
self.started = threading.Event()
# 通話状態保持用(発信or着信)
self.status = ""
# 接続状態保持用(通話中or終話)
self.disconnected = False
# ループ強制終了用プロパティ
self.alive = True
# なんだったか忘れた
self.start()
def __del__(self):
self.kill()
# 通話開始時用メソッド
def begin(self):
self.disconnected = False
self.started.set()
# 終話時用メソッド
def end(self):
self.disconnected = True
self.started.clear()
# 消灯用メソッド(プロセス キル)
def kill(self):
self.alive = False
self.disconnected = True
self.started.set()
self.join()
def run(self):
while self.alive:
# self.started.set() で ここの wait() は突破されます
self.started.wait()
# ディスプレイ初期化
lcd_init(0x08)
# 発信なら To: 電話番号 のフォーマットでセット、着信なら from: 電話番号 のフォーマットでディスプレイ表示内容をセット(1行目)
lcd_string(( " To:" if self.status == "ringing" else "from:" )+ self.line1, LCD_LINE_1)
# hour(時)をカウントするループ
for i in range(60):
# minutes(分)をカウントするループ
for j in range(60):
# secound(秒)をカウントするループ
for k in range(60):
# 通話終了ならループ丸ごとbreak
if self.disconnected:
break
# 時間をすべてループ変数から2桁で取得
hour = str(i).zfill(2)
minutes = str(j).zfill(2)
secound = str(k).zfill(2)
# Time: 00:00:00 などのフォーマットでディスプレイ表示内容をセット(2行目)
lcd_string("Time:" + hour + ":" + minutes + ":" + secound, LCD_LINE_2)
# 1秒ずつカウントアップしたのでスリープ
time.sleep(1)
# breakせずにループが実行されたら次のループへ
else:
continue
break
# breakせずにループが実行されたら次のループへ
else:
continue
break
# self.started.set() で ここの wait() は突破されます
self.started.wait()
# ここからはOSOYOOの提供するソースコードだらけなはず
# 組み込み屋さんじゃないのでよくわからないまま使わせてもらいます
# 一部自分に都合のいいよう書き換えたかも
# Define some device parameters
I2C_ADDR = 0x27 # I2C device address, if any error, change this address to 0x3f
LCD_WIDTH = 16 # Maximum characters per line
# Define some device constants
LCD_CHR = 1 # Mode - Sending data
LCD_CMD = 0 # Mode - Sending command
LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line
LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line
LCD_LINE_3 = 0x94 # LCD RAM address for the 3rd line
LCD_LINE_4 = 0xD4 # LCD RAM address for the 4th line
ENABLE = 0b00000100 # Enable bit
# Timing constants
E_PULSE = 0.0005
E_DELAY = 0.0005
#Open I2C interface
#bus = smbus.SMBus(0) # Rev 1 Pi uses 0
bus = smbus.SMBus(1) # Rev 2 Pi uses 1
def lcd_init(backlight):
# Initialise display
lcd_byte(0x33,LCD_CMD,backlight) # 110011 Initialise
lcd_byte(0x32,LCD_CMD,backlight) # 110010 Initialise
lcd_byte(0x06,LCD_CMD,backlight) # 000110 Cursor move direction
lcd_byte(0x0C,LCD_CMD,backlight) # 001100 Display On,Cursor Off, Blink Off
lcd_byte(0x28,LCD_CMD,backlight) # 101000 Data length, number of lines, font size
lcd_byte(0x01,LCD_CMD,backlight) # 000001 Clear display
time.sleep(E_DELAY)
def lcd_byte(bits, mode, backlight):
# Send byte to data pins
# bits = the data
# mode = 1 for data
# 0 for command
bits_high = mode | (bits & 0xF0) | backlight
bits_low = mode | ((bits<<4) & 0xF0) | backlight
# High bits
bus.write_byte(I2C_ADDR, bits_high)
lcd_toggle_enable(bits_high)
# Low bits
bus.write_byte(I2C_ADDR, bits_low)
lcd_toggle_enable(bits_low)
def lcd_toggle_enable(bits):
# Toggle enable
time.sleep(E_DELAY)
bus.write_byte(I2C_ADDR, (bits | ENABLE))
time.sleep(E_PULSE)
bus.write_byte(I2C_ADDR,(bits & ~ENABLE))
time.sleep(E_DELAY)
def lcd_string(message,line):
# Send string to display
message = message.ljust(LCD_WIDTH," ")
lcd_byte(line, LCD_CMD, 0x08)
for i in range(LCD_WIDTH):
lcd_byte(ord(message[i]),LCD_CHR,0x08)
def display_infomation(line1, line2):
lcd_init(0x08)
lcd_string(line1, LCD_LINE_1)
lcd_string(line2, LCD_LINE_2)
def display_backlight_off():
lcd_byte(0x01, LCD_CMD, 0x00)
ディスプレイ操作の待ち受け
着信や発信といった行為は黒電話すなわちVoIPルーターである YAMAHA NVR500 が担当する仕事ですから、 ラズベリーパイは NVR500 からのディスプレイ表示命令を何らかの手段で待ち受けなければなりません。 今回はAPIサーバーを建てることで解決しました。YAMAHAのルーターというのは基本的にHTTPでAPIを叩けますので この手法を採用しています。
以下、プログラムです。
main.py
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import LcdController
app = FastAPI()
# CORS対策などによりこれを入れないとアクセス拒否状態となります
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
# SIP状態管理用クラス
class Sip(BaseModel):
# 着信or発信の状態管理用
status: str
# 電話番号 発信元or発信先
phone_number: str
# 先ほどのディスプレイ操作用クラスのインスタンスを作成
talk_timer = LcdController.TalkTimer()
# 着信時のナンバーディスプレイ機能を http://172.16.30.40/sip/start へアクセスすることによって提供
@app.post("/sip/start")
def sip_start(sip: Sip):
LcdController.display_infomation("SIP Call from", " " + sip.phone_number)
return "done"
# 通話時間表示機能を http://172.16.30.40/sip/start へアクセスすることによって提供
@app.post("/sip/connected")
def sip_connected(sip: Sip):
talk_timer.line1 = sip.phone_number
talk_timer.status = sip.status
talk_timer.begin()
return "done"
# 終話指示受け入れ機能を http://172.16.30.40/sip/disconnected へアクセスすることによって提供
@app.get("/sip/disconnected")
def sip_disconnected():
talk_timer.end()
LcdController.display_backlight_off()
return "done"
# http://172.16.30.40/this/destroy バグってディスプレイが点灯しぱなっしのときなどに自分で消すようのパス
@app.get("/this/destroy")
def this_destroy():
talk_timer.kill()
return "done"
VoIPルーターの設定
着信や発信があったら上述したAPIを叩くというプログラムを書きルーター起動時に常駐させる設定を行いました。
プログラムの常駐
全集中プログラムの常駐はVoIPルーターが柱になるための第一歩です。続けるといい とされています。
usbにsip.luaというプログラムファイルを入れてusb1番ポートへ挿しておき、その状態で書きコンフィグを書きこんでおけば 起動時にプログラムを自動で常駐させることができます。
# show config
~ 省略 ~
schedule at 1 usb-attached * lua usb1:/sip.lua
~ 省略 ~
lua use on
プログラムの中身
YAMAHAルーターはLua言語を使ってプログラムを動かすことができます。したがって、筆者もがんばってLuaで書きました。 以下、プログラムです。
sip.lua
local rtn, str
local phone_number
local talking = false
local inviting = false
local ringing = false
local s, e, c
local base_url = "http://172.16.30.40:8000"
local content_type_text = "Content-Type: application/json"
while (true) do
-- SMARTalk関連のログを抽出
cmd = "smart"
rtn, str = rt.syslogwatch(cmd)
-- ログが見つかったら
if rtn > 0 then
-- Luaパターンに一致する文字列があれば c に代入。s と e は捨てる
s, e, c = string.find(str[rtn], "Call to %[sip:(%d-)@smart")
-- 発信状態なら
if c ~= nil then
-- 変数に電話番号を代入
phone_number = c
-- 変数を発信状態にする
ringing = true
-- デバッグ用
rt.syslog("info", "[NumberDisplay] 発信ログを検出 : " .. phone_number)
-- 着信状態か?
else
-- Luaパターンに一致する文字列があれば c に代入。s と e は捨てる
s, e, c = string.find(str[rtn], "Call from %[sip:(%d-)@smart")
-- 着信状態なら
if c ~= nil then
-- 変数に電話番号を代入
phone_number = c
-- 変数を着信状態にする
inviting = true
-- デバッグ用
rt.syslog("info", "[NumberDisplay] 着信ログを検出 : " .. phone_number)
end
end
-- 発信・着信および通話が終了
if (string.find(str[rtn], "disconnected") ~= nil) and (ringing or inviting) then
-- 発信状態なら
if ringing then
-- 変数を非発信状態にする
ringing = false
-- 着信状態なら
elseif inviting then
-- 変数を非状態にする
inviting = false
end
-- デバッグ用
rt.syslog("info", "[NumberDisplay] 切断されました")
-- API使用 ディスプレイOFF
rt.httprequest({
url = base_url .. "/sip/disconnected",
method = "GET"
})
-- 通話状態になった場合
elseif (string.find(str[rtn], "connected") ~= nil) or ringing then
-- デバッグ用
rt.syslog("info", "[NumberDisplay] 通話状態となりました")
-- API使用 通話状態を表示
rt.httprequest({
url = base_url .. "/sip/connected",
method = "POST",
content_type = content_type_text,
post_text = "{\"status\": " .. (ringing and "\"ringing\"," or "\"inviting\",") .. "\"phone_number\": \"" .. phone_number .. "\"}"
})
else
-- 着信中の場合(消去法)
if inviting then
-- API使用
rt.httprequest({
url = base_url .. "/sip/start",
method = "POST",
content_type = content_type_text,
post_text = "{\"status\": " .. (ringing and "\"ringing\"," or "\"inviting\",") .. "\"phone_number\": \"" .. phone_number .. "\"}"
})
end
end
end
end
デモンストレーション
実際に動く様子をTwitterにあげているのでご覧下さい。
金正恩+α pic.twitter.com/LgVrvzv8uw
— kusshie (@kusshie1) July 11, 2021
まとめ
APIを書いたこともなければluaに触れたこともなかったのでかなり厳しい戦いになりましたが、 動作するものが完成したのでかなり満足度は高いです。普通に黒電話を設置するよりは実用性がかなり高いので 出来る限り多くの方に試してみて貰えたら嬉しいです。最後までお読みいただきありがとうございました。