電気通信大学工学研究部 Advent Calendar 20日目の記事です。
個人的には、昨年のラズパイによるプラレール自動制御の続編(?)です。
2020年12月21日(土)~23日(月)にかけて行われた電気通信大学バーチャル調布祭にて、工学研究部と鉄道研究会は合同で遠隔・自動制御プラレール展示を行いました。
当日の様子はこちらから
調布祭期間中はとても書く暇がなかったのですが、本日から5日間、工学研究部の中の人達が、遠隔・自動制御展示の技術的なお話や裏話を徒然なるままに書いていきます。
本日の記事は、技術的よりの話よりは、鉄道よりの話が多いかもしれません。ネットワーク系の話などは、3日目~4日目あたりに掲載予定です。
目次
※リンクは適宜更新します。
- 20日:概要、自動制御部分(←この記事)
- 21日:調布祭プラレール展示のWebUIと配信の話
- 22日:調布祭プラレールのバックエンドの話
- 23日:調布祭プラレールのラズパイのお話
- 24日:プラレール車両の制御回路製作
- 25日:調布祭のお手伝いをした話
ご興味があれば鉄道研究会のHPにも配線図や展示紹介がありますので、ご覧ください。
※ノリと勢いでヒイヒイ言いながら書いていますので、誤った情報が含まれている可能性があります。もしも誤りを発見した方は、プルリクorコメント頂けると幸いです。
概要
電気通信大学のある調布ゆかりの路線として、今回は京王線・都営新宿線の特徴部分の再現をしていくことにしました。
都営新宿線区間は自動制御を行い、京王線区間はサイトにお越し頂いた方にライブ配信による遠隔操作をして頂きました。この2路線を笹塚駅のポイントを操作することで、互いに直通運転できるようにしました。
自動運転区間
走行動画:youtu.be/dw3bQZxcBbc
遠隔操作用の特設サイト
ポイントやストップレールを操作することができる特設サイトを作成しました。
chofufes-plarail-2020.takoyaki3.com
なお、展示期間終了につき実際の遠隔操作はできませんが、WebUIの操作のみであればこちらからお試しいただけます。
なお、今回の展示では操作できる列車は1編成のみとし、他はすべて分岐器・ストップレールのみを駆動させることで、列車は改造していない通常のものを使用しました。
ロジック編
自動制御区間でプログラムから扱うことができる部品は次の通りです。
パーツ名 | 個数 |
---|---|
停車用サーボモータ | 8個 |
分岐用サーボモータ | 2個 |
CDS光センサ | 1個 |
自動制御方針
都営新宿線区間を行うにあたり、次の方針を定めました。
- 2両編成は各駅停車、3両以上の編成は急行とする。
- 岩本町駅では、新宿方面・本八幡方面ともに通過待ちを行う場合がある。
- 岩本町駅で通過待ちを行う場合、各駅列車は中線に退避する。
岩本町駅の制御方針
自動制御区間で最も力を注いだのは、岩本町駅の再現です。岩本町駅は、次のような線路の配線となっています。
岩本町駅は、3セットの線路を使い、新宿方面・本八幡方面共に通過待ちを行うことができます。通過待ちを行う場合、各駅列車は中央の線路である中線に入り、本線を通過する列車の通過待ちを行います。
中央に各駅列車が入り通過待ちを行う間、反対方面の通過待ちはできません。同時に通過待ちを行うことができるのは、どちらか片方向のみとなります。
これらの要件を実現するため、ATACSが導入されていない一般的な鉄道と同様に、ストップレールごとに閉塞区間を設ける設計としました。
閉塞区間について
今回設定した閉塞区間
閉塞区間を用いた制御を行っていますが、今回の展示では列車を検知するための光センサーは笹塚にのみ設置しています。
なので、笹塚駅に来た列車を判定することはできますが、その他の区間を走る列車は、笹塚駅のセンサー情報から推測することしかできません。
今回の自動制御区間は、次のような配線となっています。
この配線図を、下の図のような7つの閉塞区間に分割しました。なお、岩本町駅の中線は新宿方面・本八幡方面共に使う併用区間であるため、③岩本町東通過待ち及び⑥岩本町西通過待ちの両方面に重複して含まれています。
閉塞区間の運用
初めに、各駅列車(青)が笹塚にやってきた場合を考えます。
各駅列車の場合、笹塚の次は、この展示の場合は九段下に停車するので、九段下までを青い各駅列車専用の区間として閉塞します。
列車が九段下駅に到着しました。まだ発車時刻になっていないので、ストップレールを停車にしておくことで、列車を停車させます。
九段下駅を発車すると、次は岩本町駅に停車します。まだ急行列車は近づいていないので、通過待ちをする必要はありません。よって、岩本町駅の本線を閉塞します。同時に、九段下駅は既に列車が存在しない状態なので、閉塞を解除します。
九段下駅到着時と同様に、岩本町駅の発車時刻になるまで九段下~岩本町の間は各駅列車の為に閉塞されます。
ここで、さらに笹塚駅に急行列車がやってくるとします。
現在、各駅列車は岩本町の本線に停車中の為、九段下~岩本町の間は各駅列車に閉塞されています。
笹塚に停車中の急行が発車する前に、各駅列車が岩本町駅を出発した場合を考えます。岩本町~本八幡の区間は、各駅列車の為に閉塞されています。急行列車は本八幡までノンストップで進みたいので、理想としては笹塚~本八幡までを急行列車の為に閉塞したいところです。
しかし、前述したように岩本町~本八幡までは各駅列車によって閉塞されているので、急行列車は笹塚~岩本町までを閉塞します。(この方式は本物の鉄道信号とは異なります)この場合、本来停車しないはずの岩本町は、岩本町より先に進むと各駅列車の閉塞区間となってしまう為、ストップレールは停車状態になります。
各駅列車が本八幡に停車している間も、急行列車は九段下を通過し、徐々に接近してきます。
各駅列車が岩本町駅を発車する前に急行列車が岩本町駅に到達してしまうと、本来停車しないはずの岩本町に停車してしまいます。一方、下の図のように急行が岩本町に到達する前に各駅列車が本八幡の折り返し線に入ると、閉塞区間が本八幡~笹塚に拡張されるので、停車することなく本八幡まで行くことができます。
各閉塞区間には、閉塞区間を通過するのに必要な最大所要時間が設定されています。
最大所要時間が経過すると、自動的に閉塞状態が解除されるようにしてあるので、しばらくすると笹塚~九段下の急行列車の為の閉塞は開放されます。
各駅列車(青)の話に戻りましょう。各駅列車が本八幡を発車する際、一つ手前の閉塞区間が急行によって閉塞されてるかを判定します。もしも一つ手前の区間、即ち、岩本町~本八幡が急行列車によって閉塞されている場合は、可能な場合、岩本町で通過待ちをするようにします。
下の図の状態において、岩本町の中線は、反対方向からの通過待ちに使用されていません。なので、中線に入るようにします。
すべての閉塞区間ごとに設定されている最大所要時間が経過するまで、閉塞状態は続きます。
中線に停車している場合、本線の本八幡から岩本町の閉塞区間は、開放されます。
そのため、本八幡を発車する急行の為に、笹塚までの全区間を閉塞することができます。
急行列車が走行している間は、各駅列車が進む先である九段下~岩本町の閉塞が開放されるまで通過待ちします。
待ちます。
急行が十分離れると、各駅列車が進むべき方向へ閉塞区間を確保します。
あとは、これまで通りに進むだけです。
なお、笹塚から先は遠隔操作区間に直通する前提でしたので、九段下から先はなにも制御していません。なのでよくぶつかります(笑)
ハードウェア編
電源などは遠隔制御区間と共有していますので、遠隔制御も含め、合計何個のモータや電源、ESPやラズパイを使用したか等は、5日目の記事に書いてくれる…はずです。
ここでは、自動制御区間で用いたものをご紹介します。
RaspberryPi
自動制御区間は、すべて岩本町のとなりに設置したRaspberryPiを使用しています。
下の写真の赤枠内の部分です。
光センサー
列車の接近及び編成長が2両か3両以上かの判定に使用しています。笹塚駅にのみ設置しています。
笹塚駅を発車する際に、光センサーが隠れていた場合は3両以上(=急行)、隠れていない場合は2両(=各駅)と判定します。
このように、レールのつなぎ目に埋め込むことで、線路を加工することなく光センサーを埋め込むことができます。
サーボモーター
サーボモータを用いて、ポイントやストップレールを動かします。サーボモータは、輪ゴムや紐、セロハンテープを駆使して接続しています。
ソフトウェア編
最後に、ソフトウェアに関してです。
今回のプログラムはPythonで実装しています。
プログラムの全体像はGithubにアップしてありますので、必要に応じてご参照ください。
ライブラリのインポート及び初期化
今回は、RaspberryPiからサーボモータ及び光センサーを扱うにあたり、RPi.GPIOを使用しました。
また、閉塞区間の時間制御を行うために、timeをインポートしています。
光センサーは1つしかなく、GPIOの21ピンに対応させました。
import RPi.GPIO as GPIO
import time
# cds
GPIO.setmode(GPIO.BCM)
GPIO.setup(21,GPIO.IN)
# init
GPIO.setmode(GPIO.BCM)
出力ピンの定義
続いて、サーボモーターの制御信号のGPIOを定義します。
なお、is_iwamoto2_direction_east
は岩本町駅の中線の方向を記憶するグローバル変数です。
iwamoto_1 = 2
iwamoto_2 = 3
iwamoto_3 = 4
iwamoto_east = 9
iwamoto_west = 17
motoyahata_1 = 11
motoyahata_2 = 10
kudanshita_1 = 27
kudanshita_2 = 22
sasazuka_1 = 5
is_iwamoto2_direction_east = True
gp_outs = [iwamoto_1,iwamoto_2,iwamoto_3,iwamoto_east,iwamoto_west,motoyahata_1,motoyahata_2,kudanshita_1,kudanshita_2,sasazuka_1]
閉塞区間の最大所要時間の設定や停車時間の設定
これらの値は実際にプラレールを走らせて値を指定しています。同じ配線構成であれば、直線レールや曲線レールを追加しても対応することができるはずです。ただ、レイアウトが大きくなればなるほど通過待ちの時間などが増えるので、停車時間の多いつまらないものになる気もしますが。。。
なお、area_time
とstop_time_rapid
により、停車時間を実際の走行にかかる時間より長く指定することで、停車駅を定めています。なお、列車の速度が異なる場合でも、最大所要時間より所要時間が短い列車であれば、速度の違いを停車時間で調整することができます。
areas = [0,1,2,3,4,5,6]
area_is_rapid = {}
stops = [iwamoto_1,iwamoto_2,iwamoto_3,motoyahata_1,motoyahata_2,kudanshita_1,kudanshita_2]
area = {}
area_time = {
0:50,
1:20,
2:30,
3:30,
4:55,
5:50,
6:100
}
stop_time = {}
stop_time_local = {
kudanshita_1:100,
iwamoto_3:80,
motoyahata_1:120,
motoyahata_2:120,
iwamoto_1:80,
iwamoto_2:150,
kudanshita_2:100
}
stop_time_rapid = {
kudanshita_1:5,
iwamoto_3:5,
motoyahata_1:120,
motoyahata_2:120,
iwamoto_1:5,
kudanshita_2:5
}
サーボモーターの初期化及び動作確認
サーボモーターの初期設定及び動作確認を行います。
動作確認を最初に入れることで、配線が物理的に正しくされていない所の判別がしやすくなりました。
servos = {}
for i in gp_outs:
GPIO.setup(i, GPIO.OUT)
servos[i] = GPIO.PWM(i, 50)
for i in gp_outs:
print(i)
servos[i].start(0)
servos[i].ChangeDutyCycle(7.25)
time.sleep(0.5)
servos[i].ChangeDutyCycle(2.5)
time.sleep(0.5)
# servos[i].stop()
for i in areas:
area[i] = 0
area_is_rapid[i] = -1
for i in stops:
stop_time[i] = -1
def up(j):
servos[j].start(0)
servos[j].ChangeDutyCycle(7.25)
def down(j):
servos[j].start(0)
servos[j].ChangeDutyCycle(2.5)
新たな列車が来た場合の判定
ここからは、実際の列車制御です。
0.1秒ごとに1ループするようにしています。
動作メカニズムは、上述した通りです。光センサーが暗くなったことで新たな列車が来た場合、一時停止し、発車時にもう一度光センサーの値を見ることで各駅/急行の判定を行います。
is_now_newtrain=0
is_first=0
c=0
while True:
time.sleep(0.1)
c=c+1
for i in areas:
area[i] = area[i]-1
for i in stops:
stop_time[i] = stop_time[i]-1
is_now_newtrain = is_now_newtrain -1
# Load New Train
if is_now_newtrain < 0:
if(GPIO.input(21)==GPIO.HIGH):
print("new train")
up(sasazuka_1)
is_now_newtrain = 80
elif is_now_newtrain == 0:
if(GPIO.input(21)==GPIO.HIGH):
print("rapid")
if area[1] < 0 and stop_time[kudanshita_1] < 0:
area[1] = area_time[1]
down(sasazuka_1)
up(kudanshita_1)
stop_time[kudanshita_1] = stop_time_rapid[kudanshita_1]
area_is_rapid[1] = 1
else:
is_now_newtrain = 1
else:
print("local")
if area[1] < 0 and stop_time[kudanshita_1] < 0:
area[1] = area_time[1]
area_is_rapid[1] = 0
down(sasazuka_1)
up(kudanshita_1)
stop_time[kudanshita_1] = stop_time_local[kudanshita_1]
else:
is_now_newtrain = 1
本八幡方面九段下
九段下を発車する時点で、笹塚駅に急行が停車中かを調べ、岩本町で通過待ちをするか判定します。なお、岩本町の中線を反対方面が使用している場合は、通過待ちを行わないようにしているので、やや判定条件が複雑になっています。
# kudanshita 1
if stop_time[kudanshita_1] == 0:
if area_is_rapid[1] == 1:
# rapid
if area[2] < 0 and stop_time[iwamoto_3] < 0:
print("kudanshita_1 rapid")
down(kudanshita_1)
up(iwamoto_3)
down(iwamoto_west)
stop_time[iwamoto_3] = stop_time_rapid[iwamoto_3]
area[2] = area_time[2]
area_is_rapid[2] = 1
else:
area[1] = area[1]+1
stop_time[kudanshita_1]=1
else:
# local
if area[2] < 0 and stop_time[iwamoto_3] < 0:
if is_now_newtrain > 0 and GPIO.input(21)==GPIO.HIGH:
# wait rapid
if area[2] < 0 and area[0] < 0 and stop_time[iwamoto_2] < 0:
print("kudanshita_1 local")
print("east wait rapid")
down(kudanshita_1)
up(iwamoto_west)
up(iwamoto_2)
area_is_rapid[0] = area_is_rapid[2] = 0
stop_time[iwamoto_2] = stop_time_local[iwamoto_3]
is_iwamoto2_direction_east = True
area[2] = area_time[2]
area[0] = area_time[0]
else:
area[1] = area[1] + 1
stop_time[kudanshita_1] = 1
else:
if area[2] < 0 and stop_time[iwamoto_3] < 0:
print("kudanshita_1 local")
print("east not wait rapid")
down(kudanshita_1)
up(iwamoto_3)
down(iwamoto_west)
area_is_rapid[2] = 0
stop_time[iwamoto_3] = stop_time_local[iwamoto_3]
else:
area[1] = area[1] + 1
stop_time[kudanshita_1] = 1
本八幡方面岩本町
岩本町駅の本線は次のように、停車時間及び閉塞のみを考慮しています。
もう少しシンプルにかけそうですね。
# iwamoto_3
if stop_time[iwamoto_3]==0:
if area_is_rapid[2]==1:
# rapid
if area[3] < 0 and stop_time[motoyahata_2] < 0:
print("start iwamoto_3 rapid")
area_is_rapid[3] = 1
down(iwamoto_3)
up(motoyahata_2)
stop_time[motoyahata_2] = stop_time_rapid[motoyahata_2]
area[3] = area_time[3]
else:
stop_time[iwamoto_3] = 1
area[2] = area[2]+1
else:
# local
if area[3] < 0 and stop_time[motoyahata_2]<0:
print("start iwamoto_3 local")
area_is_rapid[3] = 0
down(iwamoto_3)
up(motoyahata_2)
stop_time[motoyahata_2] = stop_time_local[motoyahata_2]
area[3] = area_time[3]
else:
stop_time[iwamoto_3] = 1
area[2] = area[2]+1
岩本町中線
岩本町の中線では、列車の進行方向に応じて処理を分け、本線の使用状況や急行列車の位置から発車してもよいか否かを判定しています。
# iwamoto_2
if stop_time[iwamoto_2]==0:
if is_iwamoto2_direction_east:
# to east
if area[3] < 0 and stop_time[motoyahata_2] < 0:
area_is_rapid[3] = 0
down(iwamoto_2)
up(motoyahata_2)
stop_time[motoyahata_2] = stop_time_local[motoyahata_2]
area[3] = area_time[3]
else:
stop_time[iwamoto_2] = 1
area[0] = area[0]+1
else:
# to west
if area[6] < 0 and stop_time[kudanshita_2] < 0 and area[5] < 0 and stop_time[iwamoto_1] < 0:
area_is_rapid[6] = 0
down(iwamoto_2)
up(kudanshita_2)
stop_time[kudanshita_2] = stop_time_local[kudanshita_2]
area[6] = area_time[6]
else:
stop_time[iwamoto_2] = 1
area[0] = area[0]+1
本八幡方面本八幡
終点となる本八幡は比較的単純ですね。
# motoyahata_2
if stop_time[motoyahata_2]==0:
if area[4] < 0 and stop_time[motoyahata_1] < 0:
area_is_rapid[4] = area_is_rapid[3]
down(motoyahata_2)
up(motoyahata_1)
stop_time[motoyahata_1] = stop_time_local[motoyahata_1]
area[4] = area_time[4]
else:
stop_time[motoyahata_2] = 1
area[3] = area[3]+1
新宿方面本八幡
新宿方面の本八幡では、この列車が岩本町で通過待ちをするか否かを判定したうえで列車を発車させています。
# motoyahata_1
if stop_time[motoyahata_1]==0:
if area_is_rapid[4]==1:
# rapid
if area[5] < 0 and stop_time[iwamoto_1] < 0:
area[5] = area_time[5]
down(motoyahata_1)
down(iwamoto_east)
up(iwamoto_1)
stop_time[iwamoto_1] = stop_time_rapid[iwamoto_1]
area_is_rapid[5] = 1
else:
area[4] = area[4]+1
stop_time[motoyahata_1] = stop_time_rapid[motoyahata_1]
else:
# local
if area_is_rapid[3]==1 and stop_time[iwamoto_2] < 0 and area[0] < 0:
# wait for rapid
print("wait for rapid east")
if area[5] < 0 and area[0] < 0 and stop_time[iwamoto_2] < 0:
area[0] = area_time[0]
area[5] = area_time[5]
down(motoyahata_1)
up(iwamoto_east)
up(iwamoto_2)
area_is_rapid[5] = area_is_rapid[0] = 0
stop_time[iwamoto_2] = stop_time_local[iwamoto_2]
is_iwamoto2_direction_east=False
else:
area[4] = area[4]+1
stop_time[motoyahata_1] = stop_time_local[motoyahata_1]
else:
# not wait for rapid
if area[5] < 0 and stop_time[iwamoto_1] < 0:
print("not wait for rapid west")
area[5] = area_time[5]
down(motoyahata_1)
down(iwamoto_east)
up(iwamoto_1)
area_is_rapid[5] = 0
stop_time[iwamoto_1] = stop_time_local[iwamoto_1]
else:
area[4] = area[4]+1
stop_time[motoyahata_1] = stop_time_local[motoyahata_1]
新宿方面岩本町
新宿方面岩本町の発車判定では、前の閉塞区間に急行がいるか、本線に列車が存在しているか、次の閉塞区間が開いているかを考慮しています。
# iwamoto_1
if stop_time[iwamoto_1]==0:
if area[6] < 0 and stop_time[kudanshita_2] < 0:
area_is_rapid[6] = area_is_rapid[5]
down(iwamoto_1)
up(kudanshita_2)
if area_is_rapid[5]==1:
stop_time[kudanshita_2] = stop_time_rapid[kudanshita_2]
if is_iwamoto2_direction_east==False and stop_time[iwamoto_2] >= 0:
stop_time[iwamoto_2] = stop_time[iwamoto_2]
else:
stop_time[kudanshita_2] = stop_time_local[kudanshita_2]
area[6] = area_time[6]
else:
stop_time[iwamoto_1] = 1
area[5] = area[5]+1
新宿方面九段下
最後に、新宿方面九段下です。九段下から先は遠隔操作区間なので、とてもシンプルですね。
# kudanshita_2
if stop_time[kudanshita_2]==0:
down(kudanshita_2)
以上、自動制御区間に関するお話でした。
明日は、遠隔操作用のWebUIやビデオ配信に関してです。
お楽しみに!!
--追伸--
私事ではありますが、昨年、ラズパイによるプラレール自動制御で
大学生にもなって、文化祭で鉄道研究部の展示を見てからというもの、プラレールにはまっています。
というように書いていました。プラレールに再びはまったきっかけである鉄道研究会さんと合同でできてとてもよかったです。遠隔操作を共に実現させてくれた工研・鉄研の皆さん、本当にありがとうございました。とても楽しかったです。