黒電話にナンバーディスプレイを搭載してみた

目次

背景

唐突に黒電話を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にあげているのでご覧下さい。

まとめ

APIを書いたこともなければluaに触れたこともなかったのでかなり厳しい戦いになりましたが、 動作するものが完成したのでかなり満足度は高いです。普通に黒電話を設置するよりは実用性がかなり高いので 出来る限り多くの方に試してみて貰えたら嬉しいです。最後までお読みいただきありがとうございました。




Archives

2022 (6)
2021 (3)
2020 (4)

Writer

筆者のイメージ画像
kusshie

情報系学部に所属していた社会人1年生です。大学ではネットワークを学んでいましたがまだまだです。 最近は友達に誘われてISPごっこに足を踏み入れました (AS63791)。