Raspberry Pi 相互と言っているが、Pythonが実行できればパソコンでもMacでもなんでも良いです。 両者の操作を行ったり来たりするので、SSHでの遠隔操作が望ましいでしょう。
筆者の環境
- Raspberry Pi 3 × 2
- Raspbian 9.4( stretch )
- Python 3.5.3

Raspberry Pi 3 Model B V1.2 (日本製) 国内正規代理店品
- 出版社/メーカー: Raspberry Pi
- 発売日: 2016/02/29
- メディア: Tools & Hardware
- この商品を含むブログを見る
前提
- Python3を実行できるRaspberry Piが2つある
目次
Pythonのプログラムが見たい方は5節まで飛ばしてください。
- 筆者の環境
- 前提
- 目次
- 1. ネットワーク接続
- 2. UDP送受信チェック
- 3. Pythonで送信プログラム
- 4. Pythonで受信プログラム
- 5. お互いに送受信するプログラム
- 6. インターネットを経由する
1. ネットワーク接続
Raspberry PiどうしをLANケーブルで直接接続します。
または、下図のようにハブを使って接続することもできます。ただし、うまく動作しないなどのトラブルは増えるかもしれません。
接続したら、お互いのIPアドレスなどをチェックします。 下記記事を参照してください。
ただのチェックなので、IPアドレスなどがすでに分かっている場合には飛ばして問題ありません。
2. UDP送受信チェック
次の送信の工程がうまくいくことを確認するのに必要です。 厳密には不要な工程ですが、失敗した時の原因を見極めるのに役立ちます。 面倒ならば飛ばしてください。 この節ではaptパッケージ
- tcpdump
- hping3
をインストールします。これらは次節以降の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で送信プログラム
送信側のプログラムを書きます。
文字列送信プログラム[送信側]
ソースは以下より引用し、一部改変しています。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import socket #UDP送信 import time #待機時間用 from contextlib import closing #with用 host = '169.254.169.5' # IPアドレス(変更する!) 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)) #ソケットにUDP送信 time.sleep(1) #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」の文字列が送られていることがわかります。
数値送信プログラム[送信側]
上記のソースをさらに改変します。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import socket #UDP送信 import time #待機時間用 import struct #数値→バイト列変換用 from contextlib import closing #with用 host = '169.254.169.5' # IPアドレス(変更する!) 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)) #ソケットにUDP送信 time.sleep(1) #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で受信プログラム
受信側のプログラムを書きます。
数値を受信する[受信側]
ソースは以下より引用し、一部改変しています。
import socket #UDP送信 import struct #数値→バイト列変換用 from contextlib import closing #with用 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秒毎に送ります。 パッケージの追加は必要ないはずです。
クライアント側のプログラム
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import socket #UDP送信 import time #待機時間用 import struct #数値→バイト列変換用 from contextlib import closing #with用 import ipaddress #入力IPアドレスの形式確認用 #IPアドレスの入力関係 print("Destination IP address:") while True: try: print(">",end="") #>を改行無しで表示 inputip = input() #入力させる ipaddress.ip_address(inputip) #入力が誤った形式だとエラーを吐く except KeyboardInterrupt: exit() #Ctrl+Cが入力されたらプログラムを抜ける except: print("Incorrect IP address. input IP address again.(xxx.xxx.xxx.xxx)") else: break #正しいIPアドレスだったらwhileを抜ける #送信の設定 host = inputip # 送信先(相手)IPアドレス send_port = 60000 # 送信ポート番号 #受信の設定 recv_ip = "" #このままでいい recv_port = 60000 #ポート番号 #2つのsocketを設定 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: #無限ループ #送信 # 1秒ごとに一方的に送信する print("send: ", str( s )) #送信する数値を送信側に表示 ss = struct.pack('>i', s ) #バイト列に変換 socksend.sendto(ss, (host, send_port)) #ソケットにUDP送信 #待機 time.sleep(1) #1秒待機 #受信 # パケットを受信した場合のみ、結果を表示する。それ以外は何もせずスルーする try: #try構文内でエラーが起こるとexceptに飛ぶ、なければelseへ 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
サーバ側
こちらもパッケージの追加は必要ないはずです。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import socket #UDP送信 import time #待機時間用 import struct #数値→バイト列変換用 from contextlib import closing #with用 import ipaddress #入力IPアドレスの形式確認用 #IPアドレスの入力関係 print("Destination IP address:") while True: try: print(">",end="") #>を改行無しで表示 inputip = input() #入力させる ipaddress.ip_address(inputip) #入力が誤った形式だとエラーを吐く except KeyboardInterrupt: exit() #Ctrl+Cが入力されたらプログラムを抜ける except: print("Incorrect IP address. input IP address again.(xxx.xxx.xxx.xxx)") else: break #正しいIPアドレスだったらwhileを抜ける #送信の設定 host = inputip # 送信先(相手)IPアドレス send_port = 60000 # 送信ポート番号 #受信の設定 recv_ip = "" #このままでいい recv_port = 60000 #ポート番号 #2つのsocketを設定 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)) #ソケットにUDP送信
実行結果
以下の画像のようにマスタとスレーブで通信していることがわかります。
以下の画像は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. インターネットを経由する
インターネットを経由する場合にはなにかしらの工夫が必要です. 例えばこちらなどを参照してください.