STMマイコンとROSでUDP通信

　今回はSTMマイコンとROS間のUDP通信を実装します。マイコンはSTM32F767ZI，ルーターは[TL-WR902AC](https://www.tp-link.com/jp/home-networking/wifi-router/tl-wr902ac/)，ROSはNoeticを使用します。事前にUbuntu20.04にROSとCubeIDEをインストールしておいてください。
# ROS側の実装
## 準備
まず初めにROSでプロジェクトを作りましょう。
```
$ cd ~/catkin_ws/src
$ catkin_create_pkg udp_sample roscpp rospy std_msgs
```
本筋からは逸れますが，今回のプログラムに用いるメッセージ型を実装しておきます。
```
$ cd udp_sample
$ mkdir msg
$ cd msg
$ nano SampleMsg.msg
```
以下の内容を書き込んでください。
(string型も試してみたかったのですが色々と面倒で断念しました……)
```
int32 int_sample
float32 float_sample
```
`Ctrl`+`S`，`Ctrl`+`X`で保存して閉じます。
[ROS講座10 カスタムROSメッセージ](https://qiita.com/srs/items/7ac023c549e585caeed0)に従って今作成したメッセージを使えるようにしてください。

最後にビルドしておきます。
```
$ cd ~/catkin_ws
$ catkin_make
```
## データを送信するプログラムの実装
pythonで実装していきます。
```
$ roscd udp_sample
$ cd src
$ nano send_udp.py 
```
以下の内容を書き込みます。
```py
#!/usr/bin/env python
import socket
import struct
import rospy
from udp_sample.msg import SampleMsg

def callback(data):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_address = ('192.168.0.20', 7777)
    message = struct.pack("<if",
                        data.int_sample,
                        data.float_sample)
    sock.sendto(message, server_address)
    sock.close()

if __name__ == "__main__":
    send_data = SampleMsg()
    rospy.init_node('send_udp')
    sub = rospy.Subscriber("ros_data", SampleMsg, callback, queue_size=10)
    rospy.spin()
```

<details><summary>プログラムの大まかな解説</summary><div>

プログラムが起動すると`rospy.spin()`で処理が停止し，Subscribeする状態になります。
このときにトピックを受信すると`callback()`が呼び出されます。

callback関数内のこの部分ではソケットを作成しています。`AF_INET`で IPv4，`SOCK_DGRAM`でudpプロトコルを指定します。詳しくは[公式ドキュメント](https://docs.python.org/ja/3/library/socket.html)を参照してください。
```py
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
```
ここでデータを送る相手(マイコン)のIPアドレスとポートを指定します。今回は例としてこの値を使います。
```py
server_address = ('192.168.0.20', 7777)
```
ここではデータをC言語で読み取り可能なバイナリデータに変換しています。
基本的には`struct.pack(フォーマット, 値, 値……)`という風に使います。
フォーマットの書き方などstructの詳しい使い方は[公式ドキュメント](https://docs.python.org/3/library/struct.html)を参考にしてください。
```py
message = struct.pack("<if",
                    data.int_sample,
                    data.float_sample)
```
`sock.sendto()`で先ほど指定したIPとポートに対してデータを送信します。`sock.close()`でソケットを閉じます。
```py
sock.sendto(message, server_address)
sock.close()
```
</div></details>

## データを受信するプログラムの実装
```
$ nano receive_udp.py 
```
以下の内容を書き込みます。
```py
#!/usr/bin/env python
import rospy
import socket
import struct
from udp_sample.msg import SampleMsg

class ReceiveUDP():
    def __init__(self, data_struct, server_address):
        self.data_struct = "<if"
        server_address = ('0.0.0.0', 8888)
        self.pub = rospy.Publisher("f7_data", SampleMsg, queue_size=10)
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.bind(server_address)
    def loop(self):
        self.sock.setblocking(0)
        try:
            data, address = self.sock.recvfrom(1024)
        except socket.error:
            return
        else:
            if struct.calcsize(self.data_struct) == len(data):
                f7_data = SampleMsg()
                [int_data, float_data] = struct.unpack(self.data_struct, data)
                f7_data.int_sample = int_data
                f7_data.float_sample = float_data
                self.pub.publish(f7_data)
            else:
                rospy.loginfo("received invalid data")
    def close(self):
        self.sock.close()

if __name__ == "__main__":
    rospy.init_node('receive_udp')
    receive_udp = ReceiveUDP()
    try:
        while not rospy.is_shutdown():
            receive_udp.loop()
            rospy.Rate(50).sleep()
    except KeyboardInterrupt:
        pass
    finally:
        receive_udp.close()
```

<details><summary>プログラムの大まかな解説</summary><div>

この部分で受信データの形式と，受信側(つまりPC)のIPとポートを指定し，ReceiveUDPクラスのインスタンスを作成しています。
```py
receive_udp = ReceiveUDP()
```
インスタンスを作成すると`__init__()`が呼ばれます。
`__init__()`内のこの部分で受信データの形式と，受信側(つまりPC)のIPとポートを指定します。
IPアドレスを`0.0.0.0`とすることで，PC上に存在するすべてのIPに対応することができるようです。
```py
self.data_struct = "<if"
server_address = ('0.0.0.0', 8888)
```
この部分では先程と同様にIPv4，udpプロトコルのソケットを作成し，IPとポートを紐づけています。
```py
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(server_address)
```
whileループ内では大体50HzでReceiveUDPクラスの`loop()`関数が呼び出されるようになっています。
```py
while not rospy.is_shutdown():
    receive_udp.loop()
    rospy.Rate(50).sleep()
```
`loop()`内のこの行ではソケットに受信可能なデータが来るまで処理を停止させています。
```py
self.sock.setblocking(0)
```
データを受信した場合は以下の部分が実行され，受信データが`data`に，送信元のIPが`address`に格納されます。
その際に何らかのエラーが発生した場合は`return`して再び待機状態になります。`1024`は受信バッファーサイズです。
```py
try:
    data, address = self.sock.recvfrom(1024)
except socket.error:
    return
```
上手くいった場合は次の部分が実行されます。
受信データ形式と実際のデータのサイズが等しい場合は正常なデータを受信できていると判断しています。
`struct.unpack()`は`struct.pack()`の逆で，与えられたフォーマットに従いバイナリデータを変換しています。
あとは変換されたデータをpublishしているだけです。
```py
if struct.calcsize(self.data_struct) == len(data):
    f7_data = SampleMsg()
    [int_data, float_data] = struct.unpack(self.data_struct, data)
    f7_data.int_sample = int_data
    f7_data.float_sample = float_data
    self.pub.publish(f7_data)
else:
    rospy.loginfo("received invalid data")
```
プログラムが終了する際は以下の部分が実行されソケットが閉じられます。
```py
finally:
    receive_udp.close()
```
↓
```py
def close(self):
    self.sock.close()
```

</div></details>
   
最後にファイルを実行できるようにしておきましょう。 
```
sudo chmod +x send_udp.py receive_udp.py
```
# CubeIDE側の実装
新しくプロジェクトを作成します。前述の通り今回筆者が使うボードはSTM32F767ZIなのでそれ以外を使っている方はあくまでも参考程度にお願いします。
## .iocファイルの設定
### ピン設定
(126)PG11:ETH_TX_EN
(128)PG13:ETH_TXD0
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/a09327dd-f0da-885d-a2ca-2ddcc70fed14.png)
### RCC
HSE:Bypass Clock Source
LSE:Crystal/Ceramic Resonator

### SYSの設定
Timebase Source:TIM14
### ETHの設定
Mode:RMII
 - Parameter Settings
PHY Address:0
Rx Mode:Interrupt Mode
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/e20dc623-5fa4-609b-4d07-50f82e788100.png)
### FREERTOSの設定
Interface:CMSIS_V1
MINIMAL_STACK_SIZE:256
### LWIPの設定
Enabledにチェックを入れてください。
 - General Settings
LWIP_DHCP:Disabled
IP_ADDRESS:192.168.000.020（先ほどマイコンのIPとして指定した値）
NETMASK_ADDRESS:255.255.255.000
GATEWAY_ADDRESS:192.168.000.001
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/700ab3c4-74e9-3f2e-d13d-c66f63b85365.png)
### Clockの設定
HSEに対する入力を8MHzにしてください（一番左）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/242767f3-5903-5404-a99c-d0c33bcb42e1.png)


以上の設定が終わったらGenerate Codeしましょう。








## データを送受信するプログラムの実装
### rtos_udp.h
ProjectExplorerのプロジェクト上で右クリックをして`New`>`Header File`を選択します。
`Header file:rtos_udp.h`としてヘッダーファイルを作成してください。
そこに以下の内容を書き込みます。
```c
#ifndef INC_RTOS_UDP_H_
#define INC_RTOS_UDP_H_

#include "lwip.h"
#include "lwip/sockets.h"

struct receive_data {
    int int_sample;
    float float_sample;
};

struct send_data {
    int int_sample;
    float float_sample;
};

struct receive_data GetROSData();
void SendF7Data(struct send_data*);
void UDPDefineTasks();

#endif /* INC_RTOS_UDP_H_ */
```

今回は受信したデータをそのままオウム返しするので`receive_data`と`send_data`の中身は同じにします。
### rtos_udp.c
Srcフォルダ内にrtos_udp.cというファイルを作りましょう。
そこに以下の内容を書き込みます。

<details><summary>------------（長いので畳んでいます）------------------</summary><div>

```c
#include <rtos_udp.h>
static struct receive_data ros_data = { };
static struct send_data f7_data = { };
struct timeval tv;
//IPとポート
#define F7_ADDR "192.168.0.20"
#define PC_ADDR "192.168.0.100"
#define F7_PORT 7777
#define PC_PORT 8888

osThreadId udpTaskHandle;
uint32_t udpTaskBuffer[512];
osStaticThreadDef_t udpTaskControlBlock;
void UDPSendReceive(void const *argument);

void UDPDefineTasks() {
	osThreadStaticDef(udpTask, UDPSendReceive, osPriorityNormal, 0, 512, udpTaskBuffer, &udpTaskControlBlock);
	udpTaskHandle = osThreadCreate(osThread(udpTask), NULL);
}

void UDPSendReceive(void const *argument) {
	osDelay(1000);
	fd_set reading;
	int rxsock;
    int maxfd = 0;
	char rxbuf[256]; //最大受信データサイズ
	char txbuf[256]; //最大送信データサイズ
	struct sockaddr_in rxAddr, txAddr;
    //ソケットを作成(IPv4, UDPプロトコル)
	rxsock = lwip_socket(AF_INET, SOCK_DGRAM, 0);
    //構造体の初期化
	memset((char*) &rxAddr, 0, sizeof(rxAddr));
	memset((char*) &txAddr, 0, sizeof(txAddr));
    //受信アドレスの設定
	rxAddr.sin_family = AF_INET;
	rxAddr.sin_len = sizeof(rxAddr);
	rxAddr.sin_addr.s_addr = INADDR_ANY; //全てのIPから受信
	rxAddr.sin_port = htons(F7_PORT); //マイコンのポート
    //送信アドレスの設定
	txAddr.sin_family = AF_INET;
	txAddr.sin_len = sizeof(txAddr);
	txAddr.sin_addr.s_addr = inet_addr(PC_ADDR); //PCのIP
	txAddr.sin_port = htons(PC_PORT); //PCのポート
    //ソケットにマイコンのIPとポートを紐づける
	(void)lwip_bind(rxsock, (struct sockaddr*)&rxAddr, sizeof(rxAddr));
    //ディスクリプタ集合の初期化
	FD_ZERO(&reading);
	while (1) {
		FD_SET(rxsock, &reading);
		maxfd = rxsock + 1;
		memset(&tv, 0, sizeof(tv));
        tv.tv_usec = 20000;
		(void)select(maxfd, &reading, NULL, NULL, &tv);
        //readingにrxsockが登録されているか調べる
		if (FD_ISSET(rxsock, &reading)) {
			socklen_t n;
			socklen_t len = sizeof(rxAddr);
            //rxbufに受信データを格納
			n = lwip_recvfrom(rxsock, (char*) rxbuf, sizeof(rxbuf), (int) NULL, (struct sockaddr*) &rxAddr, &len);
			if (n == sizeof(struct receive_data)) {
				//rxbufの位置にreceive_data構造体を作る
				struct receive_data *d = (struct receive_data*) &rxbuf;
				//受信データをコピーする
				memcpy(&ros_data, d, sizeof(struct receive_data));
			}
		}
        //txbufの位置にsdを宣言
    	struct send_data* sd = (struct send_data*)&txbuf;
        //送信データをコピーする
		memcpy(sd, &f7_data, sizeof(struct send_data));
		(void)lwip_sendto(rxsock, (char*)txbuf, sizeof(struct send_data), 0, (struct sockaddr*)&txAddr, sizeof(txAddr));
		osDelay(10);
	}
}

struct receive_data GetROSData(){
	//データを取得する関数
	return ros_data;
}

void SendF7Data(struct send_data *data){
	//データを送信する関数
	memcpy(&f7_data, data, sizeof(struct send_data));
}
```
</div></details>

### main.c
先程作成したヘッダーファイルをインクルードします
```c
/* USER CODE BEGIN Includes */
#include "rtos_udp.h"
/* USER CODE END Includes */
```
構造体を宣言しておきます。
```c
/* USER CODE BEGIN PFP */
static struct receive_data ros_data = {};
static struct send_data f7_data = {};
/* USER CODE END PFP */
```
RTOSのスレッドを宣言します。
```c
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
UDPDefineTasks();
/* USER CODE END RTOS_THREADS */
```
今回は以下のように変更し受信したデータをそのまま返すようにします。
```c
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
  /* init code for LWIP */
  MX_LWIP_Init();
  /* USER CODE BEGIN 5 */
  /* Infinite loop */
  for(;;)
  {
    ros_data = GetROSData();
    memcpy(&f7_data, &ros_data, sizeof(struct send_data);
    SendF7Data(&f7_data);
    osDelay(10);
  }
  /* USER CODE END 5 */
}

/* USER CODE BEGIN Header_StartSensorTask */
```
以上でCubeIDE上での作業は終わりです。コンパイルしてマイコンに書き込みましょう。



# ネットワークの設定
[ルーター](https://www.tp-link.com/jp/home-networking/wifi-router/tl-wr902ac/)の側面にあるMode SwitchをAP/Rng Ext/Clientにしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/d4724100-c222-5ebd-8fe8-6eccfd666f8a.png)
ルーターを電源に接続してからPCでWiFiに接続します。
ブラウザから`192.168.0.1`にアクセスすると，ユーザー名とパスワードを聞かれるので両者ともに`admin`と入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/7cef5929-6d16-5b93-d5a6-4797bb19d32f.png)
こんな感じの画面が表示されたら、まずワイヤレス2.4GHzを無効化します。
次にお好みでワイヤレス5GHzのワイヤレスネットワーク名を変更してください。
変更するといったんWiFiの接続が切れるので，通常のWiFiに接続しなおしてください。
ターミナルを開いて以下のコマンドを入力します。
```
$ sudo apt install network-manager
$ nmtui
```
　紫色の画面が表示されたら
`Edit a connection`→`<Add>`→`Wi-Fi`と順に選択し，`Edit Connection`画面を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/15aac6da-b2f7-14a0-1568-e4831a486c29.png)
プロファイル名は適当，
デバイスにwlp2s0，
SSIDに先程設定したネットワーク名，
セキュリティを`WPA&WPA2パーソナル`，
パスワードにルーターのパスワードを入力します。
IPv4設定をマニュアルにして，アドレスに`192.168.0.100/24`と入力します（PCのIPとして設定した値/24）。
IPv6設定を無効化します。
以上を設定出来たら`<OK>`から画面を閉じて再起動します。
```
$ nmtcli
```
ルーターのWiFiに接続できていることを確認してください。
# テスト
以上ですべての作業が終わりましたので，早速テストしてみましょう。
F7とルーターに電源を供給し，互いをコネクターでつなぎます。PCをルーターのWiFiに接続させたらターミナルを開いてください。
以下のコマンドをそれぞれ別のターミナルに打ち込みます。
```
$ roscore
$ rosrun udp_sample receive_udp.py 
$ rosrun udp_sample send_udp.py 
$ rostopic echo /f7_data
$ rostopic pub -1 /ros_data udp_sample/SampleMsg 114 5.14
```
上手くいっていればこんな感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/3fd3a899-2a3c-75e5-d7e8-990189db5c4e.png)
pubする値を変更するとechoで受け取る値も変わるはずです。

# 終わり
今回はROSserialなどに頼らず、UDPでマイコンとROSをつなぐ方法について紹介しました。やり取りするデータを変更するなどして、是非活用してみてください。