LAN内のraspiのipをudp経由で取得する

# 背景
会社と自宅などで作業するときに簡易的なサーバをラズパイで立てることがある。
ラズパイを無線LANに有線で接続し、リモートPCからラズパイにsshで接続してサーバを立ち上げている。
が、そのssh接続するときのIPアドレスが接続するWifi機器によって違う。
いちいちラズパイにディスプレイやキーボードなどをつないでIPを確認して接続するのが面倒。
固定IPにしたら楽だけど環境が頻繁に変わる（=無線LAN機器が頻繁に変わる）可能性があるので、できたら固定IP設定なしで簡単にラズパイのIPが知りたい。

と思って次のようなものを作ってみた

# 基本

- ラズパイは特定のポート**xxxx**でudpで受信待ちをする
- リモートPCがブロードキャストで任意の文字列を送信する
- ラズパイがブロードキャストされた文字列を受け取る
- 受け取った文字列が特定の文字列**yyyy**だった場合、ラズパイはリモートPCに対して答えとなる文字**zzzz**を返す。

リモートPCはラズパイから返答を返してもらえるが、どのIPから返されたかがわかるため、ラズパイのIPが何なのかがわかるという仕組み。
（安全なネットワーク内で利用する前提で、セキュリティ云々は考慮していませんのでご注意ください）

# 受信側（ラズパイ）で実装するコード

udpを受信して、特定の文字列**yyyy**が来たら、送ってきた端末に対してudp送信するコードになる。
下記のコードでは「Hello!!!」という文字列が送られてきたなら、「Hi!!!」と返すようになっている。

```python:udp_reciever.py
import socket
import datetime

def log(logstr:str):
  with open('/home/hogehoge/user_reciever_log.log','a') as f:
    timestamp = datetime.datetime.now().strftime('[%Y%m%d %H:%M:%S] ')
    f.write('{} {}\n'.format(timestamp,logstr))

def main():
  port = 8080 # ブロードキャストを受信するポート番号
  prompt = 'Hello!!!' # 「特定の文字列**yyyy**」に該当する呪文
  sock = socket.socket(socket.AF_INET, type=socket.SOCK_DGRAM)
  sock.bind(('',port))
  
  log('activate answer_daemon')
  while True:
    message, send_addr = sock.recvfrom(4096)
    log('Recv {}: {}'.format(send_addr,message.decode(errors='ignore')))
    if message == prompt.encode():
      sock.sendto('Hi!!!'.encode(),send_addr)
if __name__ == '__main__':
  main()

```

# 送信側の実装コード

ブロードキャストを投げて、その返答を待つプログラムになる。
答えが返ってこない可能性があるので5秒でタイムアウトしている。

```python:udp_sender.py
import socket
import argparse

def broadcast_sender(port:int, prompt:str):
  sock = socket.socket(socket.AF_INET, type=socket.SOCK_DGRAM)
  sock.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) # ブロードキャスト送信設定
  sock.settimeout(5) # タイムアウト設定
  sock.bind(('',0))

  try:
    sock.sendto(prompt.encode(),('255.255.255.255',port))
    msg,address = sock.recvfrom(4096)
    print('Recv {}: {}'.format(address,msg))
  except TimeoutError:
    print('Timeout')  
  
def main():
  parser = argparse.ArgumentParser(prog='sender')
  parser.add_argument('port',type=int,help='port number')
  parser.add_argument('prompt',help='prompt string')
  args = parser.parse_args()
  broadcast_sender(args.port,args.prompt)
  
if __name__ == '__main__':
  main()
```




# 実行

ラズパイ側では下記のコマンドを実行して受信プログラムを実行する
止めたかったらCtrl+CでOK.

```bash
python udp_reciever.py
```

ラズパイ側を起動したら今度はリモートPC側を実行する。
受信側は8080で待っているので、それ宛にブロードキャストを投げる

```bash
python udp_sender.py 8080 Hello!!!
```

下のような結果になったら成功

```
Recv ('192.168.**.**', 8080): b'Hi!!!'
```

この結果を以てラズパイのIPがわかる。


# udp_recieverのデーモン登録

```udp_reciever.py```はラズパイ起動時に自動で実行させたい。
systemdでデーモンとして登録すると、それが可能とのこと。

## udp_recieverにSheBang(シバン)を設定する

デーモン登録する時に[シバン](https://e-words.jp/w/%E3%82%B7%E3%83%90%E3%83%B3.html)を設定しておく必要がある。
```udp_reciever.py```の先頭に下記を追加しておく

```python:udp_reciever.py
#!/usr/bin/env python3
```

自分はvenvで仮想環境を作っていたので、下記のようなシバンを追加した。

```python:udp_reciever.py
#!/home/hogehoge/.env/bin/python
```

（任意）また、下記を実行してファイルアクセス権限を755にしておいた。

```
sudo chmod 0755 udp_reciever.py
```


## サービスファイルの記述

udp_reciever.pyをデーモン登録するにあたって特定の場所に設定ファイルを記述する
下記のコマンドを実行して、ファイルを新規作成する。
```udp_recieverd```の箇所は好きに名前を変えていい。

```bash
sudo vi /usr/lib/systemd/system/udp_recieverd.service
```

vimで開いたら下記のような内容を記述する。

```
[Unit]
Description = udp_reciever_daemon

[Service]
ExecStart = /home/hogehoge/udp_reciever.py
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target
```

ExecStartに書いてあるpythonファイルのパスは自分の環境に合わせて変えたらいい。

## サービスの稼働

service設定ファイルが書けたら下記を実行する

```bash
sudo systemctl daemon-reload        # デーモンの再読み込み
sudo systemctl start udp_recieverd  # デーモンの実行
sudo systemctl status udp_recieverd # デーモンのステータス確認（表示を見てなんか動いてそうだったらOK）
sudo systemctl enable udp_recieverd # デーモンの自動起動の設定
```


# 参考
https://my-web-note.com/raspberry-pi-python-daemon/