ArduinoのEthernetライブラリでUDPクライアントを実装する

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

Arduino UNO専用のETHERNET SHIELDを実装するとEthernetライブラリを使用してLAN通信ができます。UDP通信を実装しユニキャスト及びブロードキャスト通信でDHT20から取得した温湿度のデータを送信して動作確認を行いました。

DHT20モジュールはGrove-Temperature & Humidity Sensor V2.0(DHT20)(Seeed Studio) 及びETHERNET SHIELDはArduino Ethernet Shield 2を使用しています。

ローカルサーバーを模擬するためにESP32-WROOM-32E開発ボード(秋月電子)を使用してWiFiをサーバーモードにして使用します。下記記事で使用したサーバーを流用します。

ESP32-WROOM-32Eにサーバーを実装してレスポンスする

アクセスポイントにはWZR-HP-G300NH(バッファロー:生産中止)を使用し、ETHERNET SHIELDとLANケーブルで接続します。

ETHERNET SHIELDはアイキャッチ画像のようにArduino UNOに差し込むだけで使用することができます。Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Ethernetライブラリを使用する

Arduino Ethernet Shield Rev2を含む全体構成
Arduino Ethernet Shield Rev2を含む全体構成

ETHERNET SHIELDはイーサネットコントローラーにW5500が使用されておりSPI通信をイーサネットに変換することができます。Arduino環境でイーサネットのプロトコルであるTCP/IPやUDP/IPによる通信を構築することができます。

ローカルサーバーを模擬するためにESP32-WROOM-32Eをサーバーモード(以下サーバーESP32とします。)にして使用します。アクセスポイントとETHERNET SHIELDはLANケーブルで接続します。

UDPの特徴は全体に一斉送信するブロードキャスト(マルチキャスト)通信です。ブロードキャストはデータを送信する側の端末から接続しているすべての端末にデータを送信します。マルチキャストはネットワークのクラス全体の端末にデータを送信します。

例えば学校の全体朝礼で校長先生のお話を聞くのはブロードキャストで1年3組のクラス担任による朝礼がマルチキャストのイメージです。

複数の端末にデータを送信するブロードキャスト(マルチキャスト)に対して1対1の端末でデータを送信することをユニキャストと言いますが、相手の接続状態を確認とパケットの整合性を確認するTCPに多く使用される傾向があります。

UDPのブロードキャストとユニキャストの動作確認を行うためESP32サーバーを2台準備します。

ユニキャストでは①サーバーと②サーバーのそれぞれにArduinoから温湿度データを含む文字列を送信しサーバーからの応答を確認します。ブロードキャストでは文字列の送信に対して①と②が同時に応答するかを確認します。

EthernetライブラリはArduino UNO用に標準搭載されておりスケッチ例が各種準備されています。本記事ではスケッチ例の「UDPSendReceiveString」を流用して動作確認を行っています。

Ethernetライブラリのインクルードからを初期化及び関数の使用方法を説明します。

スポンサーリンク

ライブラリの準備と初期化

#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>

byte mac[] = {
  0x83, 0x61, 0x0A, 0xAE, 0x73, 0xA8
};

IPAddress ip(192, 168, 11, 2);//ArduinoのIP
IPAddress ipbrd(255, 255, 255, 255);//ブロードキャスト
IPAddress server(192, 168, 11, 10);//サーバーのIP
EthernetUDP Udp;

void setup() {

  Ethernet.begin(mac, ip);//mac及びipで初期化
  if (Ethernet.hardwareStatus() == EthernetNoHardware) {
    Serial.println("Ethernet shield was not found.  Sorry, can't run without hardware. :(");//イーサーネットシールドの接続が確認できない場合表示
    while (true) {
      delay(1);
    }
  }

  if (Ethernet.linkStatus() == LinkOFF) {
    Serial.println("Ethernet cable is not connected.");
  }

    Udp.begin(10002); //ポート10002でUdpポートオープン
}

EthernetライブラリはSPI通信を使用するためEthernet.hとSPI.hの2つのライブラリをインクルードする必要があります。またUDP通信を使用するためEthernetUdp.hをインクルードします。

イーサーネットコントローラーは固有のMACアドレスが割り振られているため6バイトの値を配列で準備します。ETHERNET SHIELDの裏面にMACアドレスが貼り付けてあります。

IPAddressクラスの変数としてipをインスタンス化し、引数をIPアドレスを指定します。例ではIPアドレスに「192.168.11.2」を指定しています。同様にserverをインスタンス化し、引数にサーバーのIPアドレスを指定します。例ではサーバーESP32のIPアドレスの「192.168.11.10」とブロードキャストアドレスの「255.255.255.255」を準備しています。

EthernetUDPクラスの変数としてUdpをインスタンス化(Udpオブジェクト)します。

Ethernetオブジェクトのbegin()関数でイーサーネットの初期化を行います。第1引数にMACアドレスを指定します。第2引数にIPアドレスを指定します。

hardwareStatus()関数はETHERNET SHIELDの接続を確認します。例では接続が確認できなければETHERNET SHIELDが確認できるまでループさせるようにしています。

linkStatus()関数はイーサーネットに接続状態を確認します。ケーブルの抜き差しでリンクのステータスが切り替わるため特に意識して接続状態を確認する必要はありません。

Udpオブジェクトのbegin()関数で指定したポートでUDP通信を開始します。引数にUDP通信を行うポートを指定します。

PR:スキマ時間を有効に!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!

UDPデータと準備と送信

Udp.beginPacket(server, localPort);//UDP通信パケット準備開始
//Udp.beginPacket(ipbrd, localPort);//ブロードキャスト通信の場合
Udp.print("Humidity: ");
Udp.print(temp_humi[0]);
Udp.print(" %\t");
Udp.print("Temperature: ");
Udp.print(temp_humi[1]);
Udp.print(" *C");
Udp.endPacket(); //パケット準備完了(送信)

UdpオブジェクトのbeginPacket()関数でUDPパケットの準備の初期化を行います。第1引数はサーバーのアドレスを指定します。第2引数は送信するポートを指定します。

例ではIPAddressクラスでインスタンス化したserverのIPアドレスを指定し、サーバーのポートに10002(サーバーESP32でポート10002を指定している)を指定しています。

Udpオブジェクトのprint()関数で文字列を指定します。println()関数を使用すると改行コードが最後に挿入されます。例ではDHT20モジュールから取得したデータを文字列に挿入して指定しています。

endPacket()関数でパケットの終了を発行するとUDP通信を行います。

UDPデータの受信

int packetSize = Udp.parsePacket();
if (packetSize){  
  Serial.print("Received packet of size ");
  Serial.println(packetSize);
  Serial.print("From ");
  IPAddress remote = Udp.remoteIP();
  for (int i=0; i < 4; i++) {
    Serial.print(remote[i], DEC);
    if (i < 3) {
      Serial.print(".");
    }
  }
  memset(packetBuffer,0,sizeof(packetBuffer));
  Udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);

//UDPデータの読み込み
}

UdpオブジェクトのparsePacket()関数でパケットが存在しているかを確認します。存在していれば1以上の値となるのでデータの読み込みを行います。

パケットがどの端末のものかを判別する場合はremoteIP()関数で送信元のIPを確認します。4バイトのデータで構成されているためDEC(10進数)の文字列に変換してシリアルモニターに表示するようにしています。

read()関数でパケットのデータを読み込みます。第1引数に読み出したデータを格納するアドレスを指定します。第2引数は読み込むサイズを指定します。

Arduino UNOはライブラリなどに使用するRAM容量を含めて最大で2048バイトの制限があるため読み込むサイズを可能な限り制限することが必要です。

PR:テックジム:プログラミングの「書けるが先で、理解が後」を体験しよう!

DHT20モジュールからデータを取得

Arduino IDEでDHTライブラリを追加してDHT20モジュールから温湿度のデータを取得します。Arduino IDEのライブラリマネージャの検索欄にgrove dht(ライブラリ名をそのまま入力しても良い)を入力するとライブラリの候補が表示されます。

候補の中からGrove Temperature And Humidity Sensorをインストールします。使用例は以下の通りです。

#include <DHT.h>
#include <Grove_Temperature_And_Humidity_Sensor.h>
#include <Wire.h>

DHT dht(DHT20); //DHT20を指定してインスタンス化
float temp_humi[2];

void setup() {

  Serial.begin(115200);
  Wire.begin();
  dht.begin();
}

void loop() {
  
  dht.readTempAndHumidity(temp_humi);//データを取得
  Serial.print("Humidity: ");
  Serial.print(temp_humi[0]);
  Serial.print(" %\t");
  Serial.print("Temperature: ");
  Serial.print(temp_humi[1]);
  Serial.println(" *C");
  delay(2000);
}

Arduino IDEのスケッチタブの「ライブラリをインクルード」からGrove Temperature And Humidity Sensorを選択します。エディターにDHT.h及びGrove_Temperature_And_Humidity_Sensor.hが追加されますが、DHT.hのみのインクルードでも問題ありません。DHT20はI2C通信を使用するのでWire.hも含めてインクルードします。

DHTクラスの変数としてdhtに引数を指定してインスタンス化(dhtオブジェクト)します。DHT20モジュールを使用するためDHT20を指定します。

DHT.hを選択した状態で「Go to Definition」をクリックするとDHT.hが開けるので対象のDHTモジュールのタイプを定義を確認することができます。

Wire.begin()でI2C通信の初期化を行い、dhtオブジェクトのbegin()関数でDHT20の初期化を行います。

readTempAndHumidity()関数を使用すると温湿度のデータを取得することができます。引数に取得したデータを格納するアドレスを指定します。

使用例のようにデータを取得した場所で関数をコールするだけで簡単に測定値が取得できるため手軽に動作確認できます。

スポンサーリンク

動作確認

Ethernetライブラリの動作確認の回路図
Ethernetライブラリの動作確認の回路図

ArduinoにETHERNET SHIELDを接続します。LANケーブルをアクセスポイントに接続してイーサネット通信を行います。電源を入れるとDHT20の測定が開始し、UDP通信でサーバーに温湿度データを送信します。

ArduinoはサーバーからUDPパケットを受信するとLED1(LED2)を点灯して通知します。①サーバーESP32の場合はLED1が点灯し、②サーバーESP32の場合はLED2が点灯します。

Arduinoの7ピンをGNDに接続すると①サーバーESP32が送信先になり、6ピンをGNDに接続すると②サーバーESP32が送信先になります。それ以外はブロードキャスト通信になります。

サーバーESP32は2台使用しますが、IPを切り替えるため2台目の5ピンをGNDに接続します。サーバーESP32はArduinoからUDPパケットを受信するとシリアルモニターにパケット情報を表示し、LED1ON(LED2ON)の文字列のパケットで応答します。

Arduinoから受信したUDPパケットを確認するため①サーバーESP32をシリアルモニターで確認します。電源投入後はアクセスポイントが動作開始するまで数秒間待機します。WiFi接続が確立するとサーバーとして動作開始します。

サーバーESP32のシリアルモニターの確認
サーバーESP32のシリアルモニターの確認

ArduinoからUDPパケットを受信するとUDPパケットの種別及び送信先のIPとポートを表示します。Data:以降はパケットの内容を表示しています。Arduinoから送信した温湿度データが文字列に表示できています。

サーバーESP32はUDPパケットを受信するとArduinoにUDPパケットで応答します。①サーバーESP32の場合はLED1ON、②サーバーESP32の場合はLED2ONの文字列で応答します。Arduinoのシリアルモニターを確認します。

Arduinoのシリアルモニターの結果(ユニキャスト)
Arduinoのシリアルモニターの結果(ユニキャスト)

Arduinoからユニキャストで①サーバーESP32にパケットを送信すると「192.168.11.10」からLED1ONの応答でLED1が点灯することが確認できました。また②サーバーESP32にパケットを送信すると「192.168.11.20」からLED2ONの応答でLED2が点灯することが確認できました。

Arduinoのシリアルモニターの結果(ブロードキャスト)
Arduinoのシリアルモニターの結果(ブロードキャスト)

Arduinoからブロードキャストのパケットを送信すると2つのサーバーが同時に応答し、LED1とLED2が同時に点灯することが確認できました。

広告
漠然としたキャリア形成の不安を打ち破る!

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

Arduinoのソースコード:

#include <MsTimer2.h>
#include <DHT.h>
#include <Grove_Temperature_And_Humidity_Sensor.h>
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <Wire.h>

#define TIME_UP 0
#define TIME_OFF -1
#define TIM_MEASURE 200
#define TIM_CON_WAIT 500
#define TIM_LED_ON 300
#define PIN_DI1 6
#define PIN_DI2 7
#define PIN_DO1 8
#define PIN_DO2 9

byte mac[] = {
  0x83, 0x61, 0x0A, 0xAE, 0x73, 0xA8
};

unsigned int localPort = 10002;      // local port to listen on

IPAddress ip(192, 168, 11, 2);//ArduinoのIP
IPAddress ipbrd(255, 255, 255, 255);//ブロードキャスト
IPAddress server(192, 168, 11, 10);//サーバーのIP
IPAddress server2(192, 168, 11, 20);//サーバーのIP
EthernetUDP Udp;
char packetBuffer[64];

int16_t timMeasure;
int16_t timConnect = TIM_CON_WAIT;
int16_t timled1 = TIME_OFF;
int16_t timled2 = TIME_OFF;
DHT dht(DHT20);
float temp_humi[2];

/* Local function prototypes */
void mainTimer(void);
void mainApp(void);
void ledCnt(void);

void setup() {

  Serial.begin(115200);
  pinMode(PIN_DI1, INPUT_PULLUP);
  pinMode(PIN_DI2, INPUT_PULLUP);
  pinMode(PIN_DO1, OUTPUT);
  pinMode(PIN_DO2, OUTPUT);
  Wire.begin();
  dht.begin();

  Ethernet.begin(mac, ip);
  if (Ethernet.hardwareStatus() == EthernetNoHardware) {
    Serial.println("Ethernet shield was not found.  Sorry, can't run without hardware. :(");
    while (true) {
      delay(1);
    }
  }
  if (Ethernet.linkStatus() == LinkOFF) {
    Serial.println("Ethernet cable is not connected.");
  }
  // start UDP
  Udp.begin(localPort);
  delay(1000);

  timConnect = TIM_CON_WAIT;
  MsTimer2::set(10,mainTimer); //10msごとに関数へ遷移
  MsTimer2::start();
}

void loop() {

  mainApp();
}

/* タイマ管理(timer2がタイムアップするとコールされる) */
void mainTimer(void){

  if( timMeasure > TIME_UP ){
    --timMeasure;
  }

  if( timConnect > TIME_UP ){
    --timConnect;
  }

  if( timled1 > TIME_UP ){
    --timled1;
  }

  if( timled2 > TIME_UP ){
    --timled2;
  }
}
/* メイン処理 */
void mainApp(void){

  if( timMeasure == TIME_UP ){
    timMeasure = TIM_MEASURE;

    if (!dht.readTempAndHumidity(temp_humi)) {
      Serial.print("Humidity: ");
      Serial.print(temp_humi[0]);
      Serial.print(" %\t");
      Serial.print("Temperature: ");
      Serial.print(temp_humi[1]);
      Serial.println(" *C");
    } else {
      Serial.println("Failed to get temprature and humidity value.");
    }
  }

  int packetSize = Udp.parsePacket();
  if (packetSize){  
    Serial.print("Received packet of size ");
    Serial.println(packetSize);
    Serial.print("From ");
    IPAddress remote = Udp.remoteIP();
    for (int i=0; i < 4; i++) {
      Serial.print(remote[i], DEC);
      if (i < 3) {
        Serial.print(".");
      }
    }
    memset(packetBuffer,0,sizeof(packetBuffer));
    Udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);//UDPデータの読み込み
    Serial.println(" Contents:");
    Serial.println(packetBuffer);
    ledCnt();
  }

  if( timConnect == TIME_UP ){
    timConnect = TIM_CON_WAIT;

    if( digitalRead(PIN_DI1) == HIGH && digitalRead(PIN_DI2) == LOW ){
      Udp.beginPacket(server, localPort);//UDP通信パケット準備開始
    }
    else if( digitalRead(PIN_DI1) == LOW && digitalRead(PIN_DI2) == HIGH ){
      Udp.beginPacket(server2, localPort);
    }
    else{
      Udp.beginPacket(ipbrd, localPort);//ブロードキャスト通信の場合
    }

    Udp.print("Humidity: ");
    Udp.print(temp_humi[0]);
    Udp.print(" %\t");
    Udp.print("Temperature: ");
    Udp.print(temp_humi[1]);
    Udp.print(" *C");
    Udp.endPacket();//パケット準備完了(送信)
  }

  if( timled1 == TIME_UP){
    timled1 = TIME_OFF;
    digitalWrite(PIN_DO1, LOW);
  }

  if( timled2 == TIME_UP){
    timled2 = TIME_OFF;
    digitalWrite(PIN_DO2, LOW);
  }
}

/* UDP受信後LED操作 */
void ledCnt(void){

  if( packetBuffer[0] =='L' && packetBuffer[1] =='E' && 
      packetBuffer[2] =='D' ){
      
    if(packetBuffer[3] =='1'){
      digitalWrite(PIN_DO1, HIGH);
      timled1 = TIM_LED_ON;
    }
    else if(packetBuffer[3] =='2'){
      digitalWrite(PIN_DO2, HIGH);
      timled2 = TIM_LED_ON;
    }
    else{
      digitalWrite(PIN_DO1, LOW);
      digitalWrite(PIN_DO2, LOW);
    }
  }
}

ESP32-WROOM-32E(サーバーESP32)のソースコード:

#include <WiFi.h>
#include "AsyncUDP.h"

#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIME_RECONNCT_MAX 1000
#define PIN_DI 5

const char *ssid = "xxxxxxxxxxxx"; //SSID(使用するアクセスポイントに合わせる)
const char *pass = "yyyyyyyyyyyyy"; //password(使用するアクセスポイントに合わせる)
const IPAddress ip(192,168,11,10); //IPアドレス
const IPAddress ip2(192,168,11,20); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク
  
/* 変数宣言 */
WiFiServer server(10001);
AsyncUDP udp;
uint32_t beforetimCnt = millis();
int16_t timreconnect = TIME_OFF;
int16_t waitcnt;
int8_t num;

/* プロトタイプ宣言 */
void mainApp(void);

void setup() {

  Serial.begin(115200);
  pinMode(PIN_DI,INPUT_PULLUP);

  WiFi.begin(ssid,pass);

  if(digitalRead(PIN_DI)){
    WiFi.config(ip,ip,subnet);
    num = 1;
  }
  else{
    WiFi.config(ip2,ip2,subnet);
    num = 2;
  }

  while( WiFi.status() != WL_CONNECTED ){
    delay(1000);
    Serial.print(".");
    if( ++waitcnt >= 60 ){
      esp_restart();
    }
    Serial.print(waitcnt);
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin(); //サーバーの開始

  if(udp.listen(10002)) {
    Serial.print("UDP Listening on IP: ");
    Serial.println(WiFi.localIP());
    udp.onPacket([](AsyncUDPPacket packet) {
      Serial.print("UDP Packet Type: ");
      Serial.println(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
      Serial.print("From: ");
      Serial.print(packet.remoteIP());
      Serial.print(":");
      Serial.print(packet.remotePort());
      Serial.print(", To: ");
      Serial.print(packet.localIP());
      Serial.print(":");
      Serial.print(packet.localPort());
      Serial.print(" Length: ");
      Serial.println(packet.length());
      Serial.print("Data: ");
      Serial.write(packet.data(), packet.length());
      Serial.println();
      packet.printf("LED%dON",num);//reply to the client
    });
  }
}

void loop() {

  mainApp();
}

/* メイン処理関数 */
void mainApp(void){
  
  WiFiClient client = server.available();   // listen for incoming clients

  if (client) {                             // if you get a client,
    Serial.println("New Client.");           // print a message out the serial port
    //String currentLine = "";
    while (client.connected()) {            // loop while the client's connected
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
      }
    }

    client.stop();
    Serial.println("Client Disconnected.");
  }
}

サーバーESP32のアドレスは5ピンをGNDに接続すると「192.168.11.20」に切り替えることができます。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

広告

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました