Raspberry Pi 相互と言っているが、Pythonが実行できればパソコンでもMacでもなんでも良いです。
両者の操作を行ったり来たりするので、SSHでの遠隔操作が望ましいでしょう。
筆者の環境
- Raspberry Pi 3 × 2
- Raspbian 9.4( stretch )
- Python 3.5.3
前提
- Python3を実行できるRaspberry Piが2つある
目次
Pythonのプログラムが見たい方は5節まで飛ばしてください。
1. ネットワーク接続
Raspberry PiどうしをLANケーブルで直接接続します。
または、下図のようにハブを使って接続することもできます。ただし、うまく動作しないなどのトラブルは増えるかもしれません。
接続したら、お互いのIPアドレスなどをチェックします。
下記記事を参照してください。
masa-flyu.hatenablog.com
ただのチェックなので、IPアドレスなどがすでに分かっている場合には飛ばして問題ありません。
2. UDP送受信チェック
次の送信の工程がうまくいくことを確認するのに必要です。
厳密には不要な工程ですが、失敗した時の原因を見極めるのに役立ちます。
面倒ならば飛ばしてください。
この節ではaptパッケージ
をインストールします。これらは次節以降のPythonプログラムの実行には直接関係しません。
tcpdumpをインストール[受信側]
以下のコマンドでtcpdumpをインストールします。
$ sudo apt-get install tcpdump
tcpdumpを受信状態にする[受信側]
以下のコマンドを打ちます
$ sudo tcpdump -A -n udp port 60000
すると2〜3行の
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
のような文字列がでます。これ以降、自分のIPアドレス宛に届いた60000番のUDPパケットを受信し、ターミナル上に表示する状態となります。
60000に特に意味はありません。
0〜65535であればなんでもよいです。ただし0〜49151は決まった利用目的が決められているため、この先の工程でインターネットに接続する時には、自由に使える49152〜65535をおすすめします。
この記事では60000に統一しています。
なお、受信を停止するには、Ctrl + Cを押します。
hping3をインストール[送信側]
以下のコマンドでhping3をインストールします。
$ sudo apt-get install hping3
ポートを指定してpingする[送信側]
以下のコマンドで、高度なpingを打ちます。
$ sudo hping3 -2 -p 60000 xxx.xxx.xxx.xxx
すると、tcpdumpを受信状態にしておいた受信側のターミナルに、こんなのが1秒ごとに表示されるはずです。
xx:xx:xx.xxxxxx IP xxx.xxx.xxx.xxx.xxxxx > xxx.xxx.xxx.xxx.60000: UDP, length 0
<...!..'.5....E.......@.`.......@..&.`......................
60000とUDPが確認できれば、あとはどうでもいいです。
3. Pythonで送信プログラム
送信側のプログラムを書きます。
文字列送信プログラム[送信側]
ソースは以下より引用し、一部改変しています。
rikoubou.hatenablog.com
import socket
import time
from contextlib import closing
host = '169.254.169.5'
port = 60000
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
with closing(sock):
while True:
message = 'Hello via UDP'.encode('utf-8')
print("send: ", message)
sock.sendto(message, (host, port))
time.sleep(1)
IPアドレスは受信側のものを入力してください。
以上のpythonプログラムを"udpsend.py"のような名前でどこかに保存します。
そして
$ cd (pythonプログラムのディレクトリ)
$ sudo python3 ./udpsend.py
で実行します。
すると、tcpdumpを受信状態にしておいた受信側のターミナルに
xx:xx:xx.xxxxxx IP xxx.xxx.xxx.xxx.xxxxx > xxx.xxx.xxx.xxx.60000: UDP, length 13
E..)tf.... ...@...... .`..t.Hello via UDP.....
のように表示されます。関係ない文字列が前後にありますが、「Hello via UDP」の文字列が送られていることがわかります。
数値送信プログラム[送信側]
上記のソースをさらに改変します。
import socket
import time
import struct
from contextlib import closing
host = '169.254.169.5'
port = 60000
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
with closing(sock):
while True:
d = 4.675208021
ds = struct.pack('>d', d )
print("send: ", ds)
sock.sendto(ds, (host, port))
time.sleep(1)
文字列送信プログラムから数行だけ変えています。
新たにstructをインポートする必要があります。
これを実行すると
xx:xx:xx.xxxxxx IP xxx.xxx.xxx.xxx.xxxxx > xxx.xxx.xxx.xxx.60000: UDP, length 8
E..$3.....J...@........`....@..i.@.}..........
受信側に表示されます。先ほどと違い、実行結果が入力と一致しません。
これは、バイト列を数値に戻していないためです。
そのためには受信プログラムが必要です。
4. Pythonで受信プログラム
受信側のプログラムを書きます。
数値を受信する[受信側]
ソースは以下より引用し、一部改変しています。
qiita.com
import socket
import struct
from contextlib import closing
UDP_IP = ""
UDP_PORT = 60000
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
with closing(sock):
while True:
data, addr = sock.recvfrom(1024)
print ( "bytes:", data )
print ( str( struct.unpack('>d' , data)[0] ))
送信側は先ほどのプログラム、受信側はこちらのプログラムを共に実行すると、受信側で
bytes: b'@\x12\xb3i\xbb@\xc4}'
4.675208021
が1秒ごとに表示されます。
下段の数値をプログラム内で利用することもできます。
5. お互いに送受信するプログラム
いよいよお互いに送受信させます。
シチュエーション
今までのプログラムを元に、Raspberry Piどうしでの値のやりとりを想定します。
今回は以下のようなシチュエーションとしました。
- 片方(クライアント側と呼称)から数値を送る
- もう片方(サーバ側)で受け取る
- サーバ側は値を元に計算する
- 計算結果を送る
- クライアント側で計算結果を表示する
以下のプログラムはクライアント側から数値を1秒毎に送ります。
パッケージの追加は必要ないはずです。
クライアント側のプログラム
import socket
import time
import struct
from contextlib import closing
import ipaddress
print("Destination IP address:")
while True:
try:
print(">",end="")
inputip = input()
ipaddress.ip_address(inputip)
except KeyboardInterrupt:
exit()
except:
print("Incorrect IP address. input IP address again.(xxx.xxx.xxx.xxx)")
else:
break
host = inputip
send_port = 60000
recv_ip = ""
recv_port = 60000
socksend = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sockrecv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sockrecv.bind((recv_ip, recv_port))
sockrecv.setblocking(0)
print("OK")
sum = 0.0
s = 1
with closing(socksend), closing(sockrecv):
while True:
print("send: ", str( s ))
ss = struct.pack('>i', s )
socksend.sendto(ss, (host, send_port))
time.sleep(1)
try:
sr, addr = sockrecv.recvfrom(1024)
except socket.error:
pass
else:
r = struct.unpack('>d' , sr)[0]
print ( "receive: " , str( r ))
sum += r
print(s , ": pi = " , sum * 4 )
s += 1
サーバ側
こちらもパッケージの追加は必要ないはずです。
import socket
import time
import struct
from contextlib import closing
import ipaddress
print("Destination IP address:")
while True:
try:
print(">",end="")
inputip = input()
ipaddress.ip_address(inputip)
except KeyboardInterrupt:
exit()
except:
print("Incorrect IP address. input IP address again.(xxx.xxx.xxx.xxx)")
else:
break
host = inputip
send_port = 60000
recv_ip = ""
recv_port = 60000
socksend = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sockrecv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sockrecv.bind((recv_ip, recv_port))
with closing(socksend), closing(sockrecv):
while True:
print("Waiting for receive...")
sr, addr = sockrecv.recvfrom(1024)
r = struct.unpack('>i' , sr)[0]
print ( "receive: " , str( r ))
s = 1.0 / ( 2.0 * r - 1.0 )
if r % 2 == 0 :
s = -s
print("send: ", str( s ))
ss = struct.pack('>d', s )
socksend.sendto(ss, (host, send_port))
実行結果
以下の画像のようにマスタとスレーブで通信していることがわかります。
以下の画像はMacからSSHで2台のRaspberry Piに接続しているようすです。
クライアント側の解説
7~19行目
IPアドレスをキーボードで入力させます。「xxx.xxx.xxx.xxx」の形式になっているかを確認するためにipaddressパッケージを用いています。
21~32行目
送信用と受信用のソケットをそれぞれ設定しています。
ポート番号はそれぞれ異なっていても問題ありません。
32行目の
sockrecv.setblocking(0)
でマスタの受信側ソケットをノンブロッキングに設定しています。この設定を行わないと、recvfrom()するたびになにかを受信するまでプログラムを停止してしまうため、定期的な送信ができなくなってしまいます。
39行目
with文を使用し、プログラム終了時に確実にソケットを閉じるようにします。
41~45行目
UDPパケットを送信します。
50~62行目
53行目のrecvfrom()でUDPパケットを受信します。(直近1秒間に受信した)未処理のパケットがある場合は受信して、結果がsrに格納されますが、無い場合は例外(エラー)をだしてしまいます。
そこでtry文を使ってエラーを無視するようにしています。
受信していた場合は数値化の上処理を行います。
サーバ側の解説
こちらはクライアント側よりも単純です。
7~19行目
IPアドレスをキーボードで入力させます。「xxx.xxx.xxx.xxx」の形式になっているかを確認するためにipaddressパッケージを用いています。
21~31行目
送信用と受信用のソケットをそれぞれ設定しています。
ポート番号はそれぞれ異なっていても問題ありません。
マスタ側と異なり、ノンブロッキングの設定を行いません。
ブロッキングのままで使用することで、recvfrom()でパケット受信後即時応答することが可能です。
34行目
with文を使用し、プログラム終了時に確実にソケットを閉じるようにします。
37~43行目
ノンブロッキング設定をしていないので、受信するまでrecvfrom()で実行を停止します。受診後はそれを数値化します。
46~48行目
デモ用の計算です。
50~54行目
計算結果をUDPパケットとして送ります。
プログラム全体の解説(おまけ)
本題とはややそれますが、このプログラムではマスタ側に円周率に漸近していく値が表示されます。
クライアントが数値nを送ると、サーバがライプニッツの公式のn項を返します。
それを加算していくことで、円周率を求めています。
ライプニッツの公式 - Wikipedia
整数と小数を送り合うデモのために作ってみましたが、パケット往復に1秒以上かかると値が狂ってしまいます。
6. インターネットを経由する
インターネットを経由する場合にはなにかしらの工夫が必要です.
例えばこちらなどを参照してください.
www.shujima.work