TakSan0のアイコン画像

黒電話インタフェースを制作 (ラズパイに繋げられるように)してみた

TakSan0 2021年08月27日に作成  (2021年08月31日に更新)

黒電話インタフェースを制作 (ラズパイに繋げられるように)してみた

1. はじめに

今ではあまり見かけなくなった黒電話。通称600型電話機。

黒電話

昭和育ちの我々にとってこれこそが The電話なのですよね。
どの家庭にも必ずあったものです。

  • 電話はダイヤルを回してカチカチ音を聞きながらかけるもの!!!
  • 大きいベルでリンリンなってこそなんぼの呼び出しベル!!!

絶対的存在感もありました。

最近すっかり見なくなりましたが、ジャンク屋やオークション、メルカリ等で入手できます。
ジャンク屋とか
メルカリとか
黒電話と検索すると色々出てきますし、値段によっては出たらあっという間に売り切れる人気ぶり?

というわけで
入手しました

  • 入手したら動かしたくなりますよね!
      ↓
  • どうせなら通話もしたくなりますよね!
      ↓
  • 通話ができるようになると、専用電話番号が欲しくなりますよね!
      ↓
  • 専用電話番号ができたら、今度は持ち歩きたくなりますよね!

それが当然の流れです!!!

というわけで、以下の欲求を満たすべく黒電話をハックしてみました。

  • ダイヤルを回して電話したい。
  • 馴染みのあるベルの音で着信したい。
  • 黒電話を持ち運んで携帯として使いたい。
  • 黒電話側は差し替えて使えるようにしたいので、魔改造は行わない。(他にもコレクションが増えそうなので繋ぎ変えたい)

2.ハード制作

2-1. 実現方法の検討

欲求を満たすためには SIPフォン等のIP電話サービスと連携するのがよさそうです。それにはまずネットにつながるようにしなければいけません。
なんだか、IoTっぽくなってきました…
しかしながら、携帯できるようにするには小型のPCが要ります。
ネット環境や各種サービス、そのためのプラットフォーム環境も含め、十分揃っていて携帯できるほど小型でとなると、ラズパイを使うのがよさげです。

2-2. 最初の課題

ラズパイを使うというところで、まず初めに問題となる点は、電話機とラズパイは、ハードウェアインタフェースをどうするかというところです。

そもそも、黒電話は2本線でつながっているだけで電源もありません。
欲しいのは

  • 音声入力インタフェース
  • 音声出力インタフェース
  • ダイヤルパルス信号インターフェス
  • ベル音インターフェース
  • 電源

黒電話は、これらのすべての信号を2本線で賄っています。
すごい発明だとは思います。しかし、ラズパイと接続するためには、この2本線から上記信号を分離してを取り出す必要があります。
色々と調査してこんなものを見つけました。

(新)PIC簡易疑似電話交換機キット @ 秋月電子通商

これは、2台のアナログ電話をつないで、片方の受話器を持ち上げると他方のベルが鳴りだして、取ると通話ができるというインターフォン(内線通話)の様な機能を持ちます。

疑似交換機とあるので、回線繋ぎ変え機能まであるように見えますが、通話は1対1なので繋ぎ変える機能はありません。必然的にダイヤルする必要もないためその機能はありません。
ハード的に交換機の様につながっている様な挙動をするという意味っぽいです。

欲しい機能で不足しているのは、

  • 音声入力インタフェース
  • 音声出力インタフェース
  • ダイヤルパルス信号インターフェス

音声入出力については、何処かで小型電話のジャンクを見つけてきてばらし、スピーカーとマイクを取っ払って、配線に直接つなげば何とかなります。
ダイヤルパルス信号も最悪はカチカチ音を機械学習させて音声認識で回数数えれば何とかなるかもとも思っていました。(結局この方法は不要でしたが…)

というわけで、この疑似交換機をベースに改造することに決定しました。
購入の決め手になったのは以下の3点です。

  • 回路図が公開されていてシンプルで、解析・改造しやすそうなところ。
  • 見た回路図がシンプルで理解しやすそうだったこと。
  • 制御は PICマイコンで 5V電源系の Arduinoに簡単に置き換える事ができそうなこと

なお、本記事作成に当たって、本キットの開発製造元である (有)トライステート様 に、以下の条件の基回路図の使用や改造した物を使っての記事の投稿について許諾頂いています。もし、記事を見て参考に制作等される場合はご留意ください。

許諾条件:

  1. トライステート製の「PIC疑似電話交換機キット」を使用した旨。
  2. 改造で起こった諸問題は、自己責任である旨。

2-3. 回路解析

回路図を見ると、ほぼ端子につながったリレー回路と、フォトカプラーから入力された信号の端子入力回路です。
一部オーディオ系のアンプICにつながった部分もありますが、あまり複雑な制御ではなさそうです。
制御は PICマイコンで行われています。ソフトの中身は見えないのでブラックボックスですが、リレー制御とベル鳴動の動作程度なら外から解析して、同じ動作をさせてやれば動作すると考えました。こんな感じで

回路解析

回路を読み解いていくと、入ってきた信号に対してつながったを検知し、リレーを切り替えるだけの単純な回路という事が予想付きます
アナログのオーディオ信号も、呼び出し時に呼び出し側に流れている 400Hz のトーン音を出しているのと、呼び出しベルを駆動するための 16Hz の矩形波が出ているだけであろうとの予想です。

2-4. 端子ハッキング

回路の解析結果を裏付けるために、 解析用に簡易オシロ+簡易ロジアナを作成し信号を解析しました。

Arduinoにて下の画像の通り各端子の状態を読み取れるような様な治具を作り、
端子読み取り治具

PICマイコンとICソケットの間に挟み込んで、各端子の信号の状態を読み取り Arduino のシリアルに出力します。
それをさらに、Arduino のシリアルプロッタでグラフに出力すると、こんな感じの簡易測定器になります。
簡易測定器

リレーの制御は超低速ですし、アナログ信号は周波数まで図る必要がないので、最速で回せば十分な感じです。というより、アナログ信号は図の通り速度が足りずに波形がつぶれてしまうので、別途オシロをつないで計測しました。

音声回路とベル駆動につながっている2端子はアナログ端子として analogRead() で読み取ります。
リレーの駆動回路やフォトカプラにつながっている回路は、High/Low 信号だけのはずなのでデジタル入力端子として digitalRead() で読み取ります。
そして、読み取った値にオフセット付けたり map() 関数でスケーリングしたりして、各チャンネルが重ならない様なグラフとして読み出しやすい値に変換して、Serial.print でシリアルに出力します。
こんな感じです。

Arduino_BlackTEL_Hack.ino

int DigitalData[6]; int AnalogData[2]; char buf[240]; void setup() { // put your setup code here, to run once: Serial.begin(115200); pinMode(2, INPUT); pinMode(3, INPUT); pinMode(4, INPUT); pinMode(5, INPUT); pinMode(6, INPUT); pinMode(7, INPUT); } void loop() { // put your main code here, to run repeatedly: int MappedAnalogData[2]; DigitalData[0] = digitalRead(2); DigitalData[1] = digitalRead(3); DigitalData[2] = digitalRead(4); DigitalData[3] = digitalRead(5); DigitalData[4] = digitalRead(6); DigitalData[5] = digitalRead(7); AnalogData[0] = analogRead(0); AnalogData[1] = analogRead(1); MappedAnalogData[0] = map(AnalogData[0], 0, 1023, 700, 780); MappedAnalogData[1] = map(AnalogData[1], 0, 1023, 600, 680); sprintf(buf, "%d, %d, %d, %d, %d, %d, %d, %d", MappedAnalogData[0], MappedAnalogData[1], ((DigitalData[0] == LOW)? 500: 580), ((DigitalData[1] == LOW)? 400: 480), ((DigitalData[2] == LOW)? 300: 380), ((DigitalData[3] == LOW)? 200: 280), ((DigitalData[4] == LOW)? 100: 180), ((DigitalData[5] == LOW)? 0: 80) ); Serial.println(buf); delay(5); }

将来的にもリレーの動作状況が分かりやすくなると思ったので、74LCX245 バッファを介して各端子の H/L がわかる LED も付けました。

LEDも実装

基本的に予想通りの挙動となりました。以下の画像の様に、解析結果とアナログ端子のオシロ波形画面、電話を同時に画面に映して録画することで、どういう操作をしたときにどういう波形になるかを細かく記録し、動画再生・停止・スローを駆使して解析しました。

以下は、左側の緑色電話から発呼した場合の一連の信号解析結果です。
信号解析結果1
信号解析結果2
信号解析結果3

信号の中身をまとめると、
[アナログ側]

  • (出力)トーン音 (400Hz)
  • (出力)ベル鳴動用パルス (16Hz)

[デジタル側]

  • (出力)回路反転リレー信号
  • (出力)ベル鳴動リレー信号
  • (入力)相手の回路反転検知信号

デジタル側は接続電話2チャンネル分で6端子になります。

端子調査結果
動作の状況はこんな感じです。

受話器を持ち上げると、「回路反転リレー信号」が ON になり、その状態が「相手側の回路反転検知信号」として反映され受話器が持ち上げられたことが認識できる仕組みです。
アナログ側はのトーンオンは400Hz のツー音に使用されます。他にも終話音"ツー>無音>ツー>無音" の1秒おきの繰り返し等の信号も出力されている様です。

アナログ側の「ベル鳴動パルス」は 16Hz の低い周波数の方形波を出力し、疑似交換機回路で電圧が 5V->15Vに昇圧され、それが相手のベルを叩くことになります。(16Hz という周波数でベルを1秒間に16回ならしているわけですね。)

あともう一つオリジナルの疑似交換機にはない機能である、ダイヤル時の挙動も確認します。
ダイヤルを回してみて、どのような波形が出るのかを、同様にグラフで確認します。フォトカプラーにつながった「相手の回路反転検知」端子の所にパルス信号が出てきました。これの High/Low の方形波の山の数を数えることで、何番をダイヤルしたか判別可能なことがわかります。

というわけで、ブラックボックスであるソフト制御の内容が理解できました。

2-5. 音声分離回路

さて、回線制御部分は解明したわけですが、音声信号をラズパイに入出力してやる必要があります。

前に、「音声入出力については、何処かで小型電話のジャンクを見つけてきてばらし、スピーカーとマイクを取っ払って、配線に直接つなげば何とかなります。」
とは書きましたが、大型化してしまうので何とかしたいところです。

2本線から、スピーカーとマイクの2つの信号を取り出す必要があります。
調べていくと、スピーカーとマイクで別々のオフセットがついた(中心電圧が違う)信号なのでそれを分離してやるようなものと理解しました。

色々と試行錯誤したのですが、組み入れてしまって最終的にどういう回路になったかが判らなくなってしまったのと、まだ若干マイクの音がスピーカーに混じるのでもう少しチューニングが必要です。そのため、現時点では回路図は載せません。

ですが、スピーカー側には直列にグランド側に抵抗を、マイク側には直列に信号側に抵抗を入れて、アンプを通してといったような内容の回路です。

チューニング完了してから回路含めて更新しようかと考えています。
とりあえず、混じってはいても会話ができるレベルにはなりました。

3. ソフト制作

3-1. Arduinoで PICと同等等の制御ソフト

次は、解析結果をもとにはずは、改造前の疑似交換機についていた PICマイコンの制御ソフトと、同等のものをArduinoで作ってみました。

  1. 発呼側が受話器を上げると、ベル用のリレーを操作して相手のベルを鳴らしだす。
  2. 相手が受話器を取ると回路が反転される。と同時にフォトカプラに受話器を上げたという信号が伝わる。
  3. 両方の受話器が上がったことにより、ハード的にリレーが切り替わり、音声信号のラインがつながる。
  4. 通話を開始する
    という流れです。

この制御プログラムを Arduinoで真似してやることで、見事に同じ挙動が実現できました。
そして、疑似交換機には無い機能であるダイアリングの仕組みを組み入れました。
予想通りにパルスで入ってくる方形波の数を数えるだけでした。実際パルス数を数える様な制御をソフトに組み込んでみてシリアルログに出力し、ちゃんと番号が認識できることを確認しました。

3-2. Arduinoで制御ソフト

どう制御すればいいのかがわかってきたので、次は実際の交換機動作となります。
ラズパイをモデムのように見立てて制御ができると、簡単にインターフェースとして動作させられると考えました。
そこで、ラズパイとのインタフェースは、よくあるモデムコマンドを流用した自己カスタムな(というよりまんまかも)ATコマンドを使用するようにしました。

こんな感じです。

ATコマンド 動作
AT<CR><LF> 待ち受けに戻す
ATDPxxxxxx<CR><LF> xxxxxxにダイヤルする
ATA<CR><LF> 着信に応答する
ATH<CR><LF> 切断する
応答コード 意味
<CR><LF>OK<CR><LF> OK応答
<CR><LF>ERROR<CR><LF> エラー応答
<CR><LF>CONNECT<CR><LF> 接続完了
<CR><LF>RING<CR><LF> 着信あり
<CR><LF>NO CARRIER<CR><LF> 切断された

異常系やキャンセル動作を考えるとちょっと複雑な状態遷移になるので、そのあたりや、シーケンス周りを軽く設計します。(以下一部を抜粋)

状態遷移設計

シーケンス設計の一部

主に以下の機能を実装します。

3-2-1. 発呼動作

  1. 待ち受け中に受話器を持ち上げると、ツーというトーン音を鳴らすとともに、ダイヤル待ちに入る。
  2. ダイヤルするとパルス数を数えて、ダイヤル番号を1桁ずつ確定させる。
     ツー音はダイヤル開始した時点で停止する。
     0.5sec 以下の間隔のパルス数を数え、それ以上の間隔は桁の区切りとする。
     5秒以上の間隔になった場合、呼び出し番号の確定とする。
  3. 番号が確定すると、ATコマンドにてダイヤルコマンド (ATDPxxx)を送信する
  4. 応答コードで 接続 (CONNECT)が帰ってきたら、リレーを切り替え通話状態にする。
  5. 通話状態の時に受話器を置くと、リレーをもとに戻して、回線を切断するとともに、ATコマンドにて切断コマンド (ATH) を送信する。
  6. 通話状態の時に 応答コードで切断 (NO CARRIER) が届いたら、リレーをもとに戻して、回線を切断する。
  7. 切断が確認されたら AT コマンドで、待ち受けに戻す (AT) コマンドを送信する。

3-2-1. 着呼動作

  1. 待ち受け中に ATコマンドの応答コードで 着信あり (RING)が来たら、ベルを鳴らし始める。
  2. ベルが鳴っているときに、受話器を取ったらベルを止め ATコマンドにて、着信応答 (ATA)コマンドを送信する。
  3. リレーを切り替え通話状態にする。
  4. 通話状態の時に受話器を置くと、リレーをもとに戻して、回線を切断するとともに、ATコマンドにて切断コマンド (ATH) を送信する。
  5. 通話状態の時に 応答コードで切断 (NO CARRIER) が届いたら、リレーをもとに戻して、回線を切断する。
  6. 切断が確認されたら AT コマンドで、待ち受けに戻す (AT) コマンドを送信する。

3-3. 動作確認用対抗ソフト

動作確認用に、ラズパイでArduinoと会話のお相手をするソフトを python で作成しました。
こんな感じのもので、 Arduino から ATコマンドを受けたらログを吐き出しつつ、応答コードを返すだけのソフトです。
これを使うことで、ATコマンドの応答コードを伴う状態遷移が Arduino 側で動くためテストすることができるようになります。

  • CALL<Enter> と打つとATコマンド応答コードの 着信あり(RING) を送信して、黒電話を着信状態にします。

  • その後黒電話の受話器が取られて、ATコマンドの着信応答 (ATA) を受信すると、 OK を返し、Arduino側を通話状態にします。

  • 受話器が置かれて、ATコマンドの切断 (ATH) を受信したら、OKを返し、Arduino 側で切断処理を実行させて、待ち受けに戻します。

  • Arduino側が待ち受けに戻って ATコマンドの待ち受け(AT) を受信すると、OKを返します。

  • EXIT<Enter> と打つと終了します。

改造した部分でもなくなんの柵もないのと、大したことやっていないのでソースも公開します。

こんな感じです。

BlkPhoneAnswTest.py

import time import threading as th import serial import datetime import SimpleQueue AnsweringCount = 7 TalkingTimeout = 30.0 is_active = True ring_count = 0 is_talking = False is_serial_ready = False is_queue_ready = False def key_watch(): global in_str global is_active global is_calling in_str = input().upper() is_calling = False while (in_str != 'QUIT' and in_str != 'Q' and in_str != 'EXIT' and in_str != ''): if ( ( is_calling == False) and (in_str == 'CALL' or in_str == 'C') ): is_calling = True ser.write(b'\r\nRING\r\n') print(timestamp()+"[PYTHON3]$>RING") in_str = input().upper() is_active = False def initialize_serial(): global ser global is_talking global is_calling global is_serial_ready print("[Black-TEL python answering Test] Ver. 0.03") ser = serial.Serial('/dev/ttyACM0', 115200, timeout = 0) is_talking = False is_calling = False print(timestamp()+"[PYTHON3]$Serial Ready!") is_serial_ready = True def thread_read_serial(): global que global ser global is_active global is_serial_ready global is_queue_ready que = SimpleQueue.SimpleQueue(1024) is_queue_ready = True print("thread_read_serial Started!") while ((is_serial_ready == True) and (is_active == True)): data = ser.read(1) if ( len(data) != 0 ): que.Put(data) #print(data, len(data)) print("thread_read_serial Ended!") def read_line_serial(): global que global is_active global is_queue_ready #line = ser.readline() ret_found = False line = b'' while ((is_queue_ready == True) and (is_active == True) and (ret_found == False)): if (que.DataExists() == True): ret, data = que.Get() if (ret == True): line += data if (data == b'\n'): ret_found = True else : print("$BufferOverRun") return line def loop_serial(): global is_talking global is_calling global ring_count global disconnect_time line = read_line_serial() utf8_line = line.decode(encoding='utf8',errors='replace').replace('\n','').replace('\r','') if (line != b''): if (line == b'AT\r\n'): print(timestamp()+"[PYTHON3]$<" + utf8_line) ser.write(b'\r\nOK\r\n') print(timestamp()+"[PYTHON3]$>OK") elif (line == b'ATA\r\n'): disconnect_time = time.time() + TalkingTimeout is_talking = True is_calling = False ring_count = 0 print(timestamp()+"[PYTHON3]$<" + utf8_line) ser.write(b'\r\nOK\r\n') print(timestamp()+"[PYTHON3]$>OK") elif (line == b'ATH\r\n'): is_talking = False is_calling = False print(timestamp()+"[PYTHON3]$<" + utf8_line) ser.write(b'\r\nOK\r\n') print(timestamp()+"[PYTHON3]$>OK") elif ( ( len(utf8_line) >= 4) and (utf8_line[0:4] == 'ATDP')): call_number = utf8_line.replace('ATDP', '' ) print(timestamp()+"[PYTHON3]$<" + utf8_line) print(timestamp()+"[PYTHON3]$DIALING>>" + call_number) ser.write(b'\r\nOK\r\n') print(timestamp()+"[PYTHON3]$>OK") elif ( ( len(utf8_line) >= 11) and (utf8_line[0:11] == '|Bell = OFF') and (is_calling == False) ): if ( ring_count < AnsweringCount ): ring_count += 1 else : ser.write(b'\r\nCONNECT\r\n') print (timestamp()+"[PYTHON3]$>CONNECT") disconnect_time = time.time() + TalkingTimeout is_talking = True is_calling = False ring_count = 0 else: print(timestamp()+"[ARDUINO]" + utf8_line) if (is_talking == True): if ( disconnect_time < time.time() ): ser.write(b'\r\nNO CARRIER\r\n') is_talking = False is_calling = False print(timestamp()+"[PYTHON3]$>NO CARRIER") def finalize_serial(): global is_serial_ready is_serial_ready = False ser.close() print("Serial Closed!") def timestamp(): timestr = (datetime.datetime.now().strftime('%m%d%H%M%S.%f')) return timestr def main_process(): th.Thread(target = key_watch, args = (), name = 'key_watch', daemon = True).start() ser = initialize_serial() th.Thread(target = thread_read_serial, args = (), name = 'read_serial', daemon = True).start() while is_active: loop_serial() finalize_serial() main_process()

SimpleQueue.py

class SimpleQueue: def __init__(self, _size): self._Size = _size self._DataBuffer = [0] * (self._Size+1) self.Clear() print("Queue size of " + str(self._Size) + " ready") def Clear(self): self._PutPos = 0 self._GetPos = 0 def Put(self,data): if (self._PutPos < self._Size): NextPos = self._PutPos + 1 else: NextPos = 0 if (NextPos == self._GetPos): return False else: self._DataBuffer[self._PutPos] = data self._PutPos = NextPos return True def Get(self): if (self._GetPos == self._PutPos): #print(f"No data for GET{self._PutPos},{self._GetPos}") return False if (self._GetPos < self._Size): NextPos = self._GetPos + 1 else: NextPos = 0 data = self._DataBuffer[self._GetPos]#.encode() self._GetPos = NextPos return True, data def DataExists(self): if (self._GetPos == self._PutPos): return False else : return True

3-4. 音声疎通確認

音声の疎通確認を行うために専用のソフトを Python で書こうと考えました。
受話器で話した音声を3秒後にエコーバックするという単純なものです。

実現方法を考えていると複雑になりそうで迷ったのですが、よく考えるとラズパイに搭載されたALSA(ローンじゃないほうのやつ) の標準機能だけで確認はできるので、そちらを利用しました。

スピーカー側テスト

ラズパイからサイン波を再生してやり、黒電話受話器のスピーカーから聞こえることを確認します。

speaker-test -t sine -f 400

いい感じです。

マイク側テスト

arecord -f cd test.wav

で黒電話受話器のマイクから、よくあるセリフ
「もしもし、ただいまマイクのテスト中」
で録音してやり。

aplay test.wav

それを、再生して黒電話受話器のスピーカーから聞こえることを確認。
ノイズは多いですが、いい感じです。

いけそうなのでこれでOKとしましたが、落ち着いたらエコーバックのソフトも作ってみようと思います。

最後に、以上の解析からテストまでの様子を動画にまとめたものを
@デモ動画に
として掲載します。

4. まとめ

ハックから始まってガチな回路制作まで経て、何とかラズパイと黒電話機のインターフェースを作成することができました。

システム構成図的にはこのようになります。

システム構成図

ラズパイとインターフェースができてしまえば、あとは IPフォンと連携するだの、電話会議システムと連携するだの、Twilio 等の電話サービスと連携するだのすれば、電話として使うことができるだの、いろいろと応用がききそうです。

現在、
ちょうど Twilio オンラインコンテスト2021 が開催されて
いるので、Twilio との連携に挑戦していきたいと思います。
いたので、Twilio との連携に挑戦しました。

その挑戦については別途…
⇒応募が完了したので、そちらの記事も公開します。
コンテストの規定により、記事は別サイト Protopedia での掲載となります。

コンテスト記事:

昭和育ちなおっさんたちは黒電話を携帯する夢を実現する...

TakSan0のアイコン画像
大阪在住の本業組込み系SEです。ハードに近いところのソフト屋さんです。 電子工作は趣味でやっています。 よろしくお願いします。 ▼こちらにも別の作品を投稿しています。参考まで。 https://protopedia.net/prototyper/taksan
  • TakSan0 さんが 2021/08/27 に 編集 をしました。 (メッセージ: 初版)
  • TakSan0 さんが 2021/08/28 に 編集 をしました。 (メッセージ: (有)トライステート様許諾に伴い修正)
  • TakSan0 さんが 2021/08/31 に 編集 をしました。 (メッセージ: 公開用に編集+動画追加)
  • TakSan0 さんが 2021/08/31 に 編集 をしました。 (メッセージ: タグの付け忘れ修正。)
  • TakSan0 さんが 2021/08/31 に 編集 をしました。 (メッセージ: Twilioコンテスト応募完了に伴う変更)
ログインしてコメントを投稿する