Raspberry PiどうしでUDPやりとり(Python 3)

Raspberry Pi 相互と言っているが、Pythonが実行できればパソコンでもMacでもなんでも良いです。 両者の操作を行ったり来たりするので、SSHでの遠隔操作が望ましいでしょう。

筆者の環境

前提

目次

Pythonのプログラムが見たい方は5節まで飛ばしてください。

1. ネットワーク接続

Raspberry PiどうしをLANケーブルで直接接続します。

f:id:masa_flyu:20180716033616p:plain

または、下図のようにハブを使って接続することもできます。ただし、うまく動作しないなどのトラブルは増えるかもしれません。

f:id:masa_flyu:20180716034706p:plain

接続したら、お互いの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 #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」の文字列が送られていることがわかります。

数値送信プログラム[送信側]

上記のソースをさらに改変します。

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で受信プログラム

受信側のプログラムを書きます。

数値を受信する[受信側]

ソースは以下より引用し、一部改変しています。

qiita.com

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. 片方(マスタ側と呼称)から数値を送る
  2. もう片方(スレーブ側)で受け取る
  3. スレーブ側は値を元に計算する
  4. 計算結果を送る
  5. マスタ側で計算結果を表示する

以下のプログラムはマスタからの数値に1足して送り返すだけの簡単な処理を1秒毎に繰り返します。 パッケージの追加は必要ないはずです。

マスタ側のプログラム

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

スレーブ側

こちらもパッケージの追加は必要ないはずです。

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に接続しているようすです。 f:id:masa_flyu:20180716020327p:plain

マスタ側の解説

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. インターネットを経由する