PR

Arduino UNO R4 WiFiでWebServerを実装する

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

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

Arduino UNO R4 WiFiのWiFiライブラリとSDライブラリを使用するとSDカードに保存したHTMLファイルで応答するWebServerを実装することができます。地磁気センサーのデータをブラウザーに表示して動作確認しました。

WiFiライブラリでアクセスポイントを実装する方法とSDカードを操作する方法は下記記事にまとめています。

Arduino UNO R4 WiFiでアクセスポイントを実装する

Arduino UNO R4 WiFiでSDカードを操作する

本記事はこれらの記事で使用したライブラリを組み合わせた応用例です。

SD CARD SHIELDはアイキャッチ画像のようにArduino UNOに差し込むだけで使用することができます。地磁気センサーモジュールにAE-BM1422AGMV(秋月電子で購入、以下BM1422と表記します)を使用しています。

以下ではArduino UNO R4 WiFiをUNOR4-WiFiと表記します。Arduinoのライブラリを使用して動作確認したことをまとめています。

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

WebServerの全体構成

WebServerの全体構成
WebServerの全体構成

スマホはUNOR4-WiFiで実装したアクセスポイントに接続し、Google Chromeなどのブラウザーを使用して「192.168.11.1」にアクセスします。

ブラウザーはクライアントとして動作し、最初にGET / HTTPを送信します。UNOR4-WiFiはこの文字列を識別してSDカードからhtmlファイル(sdwifi.txt)を読み込んで応答します。

次にブラウザーはUNOR4-WiFiから取得したHTML内のスクリプトの読み込みのため、GET /js.txtを送信します。UNOR4-WiFiはこの文字列を識別してSDカードからJavaScriptファイル(js.txt)を読み込んで応答します。

その後、JavaScriptの処理によって一定周期でGET /dataが送信され、UNOR4-WiFiはセンサーのデータをJSON形式で応答します。

SDカードを使用することでString型の変数でHTMLデータを生成する方法よりも大容量のデータで応答することができるようになります。

以下ではWiFiClientクラス、SDクラス、Fileクラスのメンバー関数を赤太文字で記載し、その他のメンバー関数は太文字で記載します。

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

クライアントの接続と応答

WiFiClient client = server.available();
if (!client) return; //接続なしで抜ける

String req = client.readStringUntil('\r');
client.readStringUntil('\n');
Serial.println(req);

if (req.startsWith("GET / HTTP")) {
  File myfile = SD.open(htmpath);
  if (!myfile) {
    client.println("HTTP/1.1 404 Not Found\r\n");
    Serial.println("file doesn't exist.");
    return;
  }

  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println("Connection: close");
  client.println();

  while (myfile.available()){
    client.write(myfile.read());
  }

  myfile.close();
}

client.stop();

1行目のavailable()関数でクライアント(ブラウザー)からの接続を待機します。接続要求があると4行目に進みます。クライアントからHTTPデータのリクエストがGET / HTTP/1.1のようにバージョン付きで送信されます。

HTTPの行末は改行コードのCRLF(\r\n)が付くため、readStringUntil()関数で文字列の先頭から\r\nまで読み込んでリクエストの内容を判断します。readStringUntil()関数は1文字しか指定できないため、CRLF(\r\n)の2文字を一度に処理できません。そのため先にCR(\r)までを読み、5行目でLF(\n)を読んでLFが残らないようにしています。

Arduinoの公式サンプルなど多くの場合、CRLFを2段階で処理する方法で実装されています。

8行目はリクエストから取得した文字列がGET / HTTPから始まっている文字列であるかをstartsWith()関数で判定します。HTTPリクエストは環境によってバージョンが変わるためバージョンを含めずにGET / HTTPまでを判定することで、トップページのリクエストを判定することができます。

例では省略していますが、JavaScriptのリクエストはGET /js.txtのようにHTTPデータで指定したファイル名が入るためトップページのリクエストと区別することができます。

9行目はSDカードのファイルを開いています。ファイルが存在しない場合は”HTTP/1.1 404 Not Found\r\n”でページが存在しないことをprintln()関数でブラウザーに通知します。

ブラウザーに最初にリクエストが正常に処理できたことを通知するため、HTTPのバージョン(HTTP/1.1)、ステータス(200 OK)を送ります。 200 OKはリクエストが正常に処理されたことを通知する応答です。

「Content-Type: text/html」は送るデータが HTMLであることを通知するヘッダーです。「Connection: close」はファイルを送信した後サーバー(UNOR4-WiFi)の接続を閉じる通知です。UNOR4-WiFiで最後にstop()関数で切断をしますが、事前に接続を閉じる通知をしておくことでブラウザー側も切断処理にスムーズに進むため通信が安定します。

ヘッダーの後は空行をいれてからHTMLファイルのデータを送信します。

21~22行目はHTMLデータをSDカードから読み出してブラウザーに送信します。HTMLデータが正常であればページが表示できます。ファイルの送信後はclose()関数でファイルを閉じます。

最後にクライアントの接続をstop()関数で切断します。

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

HTMLファイルの生成

<body>
<h1>現在の測定値</h1>
<div>X軸: <span id="x">---</span></div>
<script src="js.txt"></script>
</body>

HTMLファイルのJavaScriptを使用して値を更新する仕組みをソースコードから一部を抜粋して説明します。

3行目はX軸の値を表示します。初期の文字は「—」になりますが、後述のJavaScriptによって取得した値がid=”x”と連動して更新されて表示されます。

4行目はHTMLがJavaScriptファイルを読み込むためのものです。src=”xxx”のxxxに指定した外部のJavaScriptファイルを読み込みます。

通常拡張子は.jsを使いますがSDカードライブラリの制約により8.3形式を越えないようにファイル名を指定する必要があるため.txtですべての拡張子を統一しています。

SDカードにはjs.txtとして保存していますが、応答のヘッダーでJavaScriptファイルであることを通知できるため問題ありません。

ブラウザーからGET /js.txtのリクエストを受けたときHTTPファイルと同様にソースコードで応答しますが、Context-Typeの種別を以下のように指定します。

client.println("Content-Type: application/javascript");

「Content-Type: application/javascript」を返すことでブラウザーはテキストファイル(.txt)であってもJavaScriptファイルと認識して処理します。

JavaScriptファイル

async function update() {
  try {
    const r = await fetch("/data");
    const j = await r.json();

    document.getElementById("x").textContent = j.x;
  } catch(e) {
    document.getElementById("x").textContent = "ERR";
  }
}

setInterval(update, 1000);
update();

updete()関数はfetch(“/data”)でUNOR4-WiFiに接続し、生成しているJSONデータを取得します。取得したJSONのxの値をHTML内のid=”x”の要素に表示します。このupdate()関数はsetInterval()関数で1000ms(1秒)毎にコールされてブラウザーの値が自動的に更新されます。

update()関数にasyncをつけると関数内でawaitが使えるようになります。JavaScriptのawaitは非同期処理が終わるまで、この関数の処理を一時停止する意味があります。ただし、Arduinoのdelay()関数のようにCPUを待機させるのではなく、関数の中だけを一時停止するだけで、ブラウザーの処理に影響は与えません。

fetch()関数はJavaScriptの標準関数で、指定したURLにGET リクエストを送り、UNOR4-WiFi(サーバー)から応答(レスポンス)を受け取るまでを自動で行います。3行目のawait fetch(“/data”)は通信でJSONデータを取得するのを待って次の行に進みます。

昔のJavaScriptは関数のコールバックの中に処理を追加していく入れ子のような構造で可視性が悪かったこともありましたが、asyncとawaitを使うと上から流れるコードになるため可視性が良くなるメリットがあります。

4行目のawait r.json()ようにしてJSONとしてデータを取得することができます。

PR:技術系の通信教育講座ならJTEX

JSONファイルを生成する

String json;

json = "{";
json += "\"x\":" + String(mag[0])  + ",";
json += "\"y\":" + String(mag[1]) + ",";
json += "\"z\":" + String(mag[2]);
json += "}";

BM1422から取得したデータからJSONの形式に合わせて文字列を作成します。JSONはキーと値の組み合わせを{}で囲んだデータ形式です。キーは必ず” “で囲み値には数値や文字列が入ります。キーと値は:(コロン)でつなげてペアをつくり、ペアが複数あるときは,(カンマ)で区切ります。

例を挙げると{”x”:123,”y”:456,”z”:789}は”x”が123、”y”が456、”z”が789になります。

スポンサーリンク

動作確認

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

SD CARD SHILDをUNOR4-WiFiに挿入して動作確認を行います。BM1422はSD CARD SHILDのWireのピンに配線していますがSD CARD SHILDはUNOR4-WiFiのピンを延長しているだけなので配線は同じです。

最初にSDカードに「sdwifi.txt」、「js.txt」のファイルを作成します。ファイルはソースコード全体のものをテキストファイルで保存しています。

SDカードにファイルを作成する
SDカードにファイルを作成する

UNOR4-WiFiの電源を入れるとBM1422の測定とクライアントからの接続を待機します。スマホでアクセスポイントに接続して、Chromeなどのブラウザーで「192.168.11.1」を入力して接続するとHTMLファイルの内容が表示されます。

ブラウザーで表示した結果
ブラウザーで表示した結果

UNOR4-WiFiで生成したJSONが展開されてブラウザーに表示されています。BM1422は磁気センサーなので磁石を近づけたり離したりすると値が変化します。

シリアルモニターの結果
シリアルモニターの結果

シリアルモニターで測定タイミングとブラウザーのリクエストについて確認しました。BM1422のソフトウェアタイマを 1秒周期に設定していても、メインループの処理が重くなると2~3秒程度に伸びてしまっています。

これはWebServer、SDカード、センサー読み取りが重なるとUNOR4-WiFiのタスク処理で負荷が高くなることが原因です。

リアルタイム性が必要な用途では周期が安定しないため、頻繫な更新が不要なセンサー(温湿度センサーなど)のログを保存したデータを表示するロガーの用途が最適だと思います。

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

ソースコード全体

ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。

UNOR4-WiFiのソースコード:

#include <Wire.h>
#include <SD.h>
#include "WiFiS3.h"

#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIME_BM1422 100 //BM1422の計測タイマ  
#define TIME_OUT 20  //BM1422の通信タイムアウト
#define TIME_LED 50
#define BM1422_ADRS 0x0E
#define CTRL1_ADRS 0x1B
#define CTRL2_ADRS 0x1C
#define CTRL3_ADRS 0x1D
#define CTRL4_ADRS 0x5C
#define CTRL4_ADRS2 0x5D
#define STA1_ADRS 0x18
#define DATA_ADRS 0x10

#define CTRL1_SET 0xC2
#define CTRL2_SET 0x0C //未使用
#define CTRL3_SET 0x40
#define CTRL4_SET 0x00

#define SD_CS 4

typedef enum{
  BM1422_STEP1 = 0,
  BM1422_STEP2,
  BM1422_STEP3,
  BM1422_STEP4,
  BM1422_STEP5,
  BM1422_IDLE,     
  BM1422_MAX  
}BM1422_MODE;

uint32_t beforetimCnt = millis();
int16_t timbm1422;
int16_t timled;
int16_t timMeas;
BM1422_MODE md;
uint8_t cmadbuf[3]; //送信するデータを格納
uint8_t mdata[6]; //測定データ換算前
float mag[3]; //換算後データ
String htmpath = "/sdwifi.txt"; //8.3形式
String jsonpath = "/js.txt"; //8.3形式
int status = WL_IDLE_STATUS;
WiFiServer server(80);
const char *ssid = "EngKapi1"; //SSID
const char *pass = "22223333"; //password
const IPAddress ip(192,168,11,1); //IPアドレス
String json;

void mainApp(void);
void mainBm1422(void);
void mainTimer(void);
bool BM1422Cmd(uint8_t* cmd, uint8_t len);
bool BM1422GetData(uint8_t adr, uint8_t* reg_data, uint16_t len);
void MagSet(void);
void JsonSet(void);

void setup() {

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

  if(!SD.begin(SD_CS)){
    Serial.println("initialization failed!");
    while (1);
  }
  
  Serial.println("initialization done.");

  WiFi.config(ip);
  status = WiFi.beginAP(ssid, pass);
  if (status != WL_AP_LISTENING) {
    Serial.println("Creating access point failed");
    // don't continue
    while (true);
  } 

  server.begin();

}

void loop() {
  mainTimer();
  mainBm1422();
  mainApp();
}

/* タイマ管理 */
void mainTimer(void){

  if ( millis() - beforetimCnt > BASE_CNT ){
    beforetimCnt = millis();

    if( timbm1422 > TIME_UP ){
      --timbm1422;
    }
    if( timled > TIME_UP ){
      --timled;
    }

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

  WiFiClient client = server.available();
  if (!client) return;

  String req = client.readStringUntil('\r');
  client.readStringUntil('\n');
  Serial.println(req);

  if (req.startsWith("GET / HTTP")) {
    //HTMLファイルの読み出し
    File myfile = SD.open(htmpath);
    if (!myfile) {
      client.println("HTTP/1.1 404 Not Found\r\n");
      Serial.println("file doesn't exist.");
      return;
    }

    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: text/html");
    client.println("Connection: close");
    client.println();

    while (myfile.available()){
      client.write(myfile.read());
    }

    myfile.close();
  }
  else if( req.startsWith("GET /data") ){
    //JSONファイルを送信
    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: application/json");
    client.println("Connection: close");
    client.println();
    client.print(json); 
  }
  else if (req.startsWith("GET " + jsonpath) ) {
    //JavaScriptファイルの読み出し
    File myfile = SD.open(jsonpath);

    if (!myfile) {
      Serial.println("file doesn't exist.");
      return;
    }

    client.println("HTTP/1.1 200 OK");
    client.println("Content-Type: application/javascript");
    client.println("Connection: close");
    client.println();

    while (myfile.available()){
      client.write(myfile.read());
    }

    myfile.close();
  }
  else {
    client.println("HTTP/1.1 404 Not Found\r\n");
  }

  client.stop();
}

/* BM1422測定 */
void mainBm1422(void){

  switch(md){
    case BM1422_STEP1:
      cmadbuf[0] = CTRL1_ADRS;
      cmadbuf[1] = CTRL1_SET;

      if( BM1422Cmd(cmadbuf, 2)){
        timbm1422 = TIME_OUT;
        md = BM1422_STEP2;
      }
      else{
        Serial.println("STEP1 NG");
      }
      break;
    case BM1422_STEP2:
      if( timbm1422 == TIME_UP ){
        cmadbuf[0] = CTRL4_ADRS;
        cmadbuf[1] = CTRL4_SET;
        cmadbuf[2] = CTRL4_SET;

        if( BM1422Cmd(cmadbuf, 3) ){
          timbm1422 = TIME_OUT;
          md = BM1422_STEP3;
        }
        else{
          Serial.println("STEP2 NG");
        }
      }
      break;
    case BM1422_STEP3: //データ測定開始
      if( timbm1422 == TIME_UP ){
        cmadbuf[0] = CTRL3_ADRS;
        cmadbuf[1] = CTRL3_SET;

        if( BM1422Cmd(cmadbuf, 2 )){
          timbm1422 = TIME_OUT;
          md = BM1422_STEP4;
        }
        else{
          Serial.println("STEP3 NG");
        }
      }
      break;
    case BM1422_STEP4:
      if( timbm1422 == TIME_UP ){
        timbm1422 = TIME_OUT;

        uint8_t sta;
        BM1422GetData(STA1_ADRS, &sta, 1);

        if( sta &= 0x40 ){
          md = BM1422_STEP5;
          //Serial.println("Measure OK");
        }
      }
      break;
    case BM1422_STEP5:
      if( timbm1422 == TIME_UP ){
        timbm1422 = TIME_OUT;

        BM1422GetData(DATA_ADRS, mdata, sizeof(mdata));
        MagSet();
        md = BM1422_IDLE;
        timMeas = TIME_BM1422;
      }
      break;
    case BM1422_IDLE:
      if( timMeas == TIME_UP ){
        timMeas = TIME_OFF;
        md = BM1422_STEP3;
      } 
      break;
  }

  if( timled == TIME_UP ){
    timled = TIME_LED;
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }

}
/* 磁気センサーの値を換算 */
void MagSet(void){
  int16_t x;
  int16_t y;
  int16_t z;

  x = (mdata[1] << 8) + mdata[0];
  y = (mdata[3] << 8) + mdata[2];
  z = (mdata[5] << 8) + mdata[4];

  mag[0] = (float)x * 0.042;
  mag[1] = (float)y * 0.042;
  mag[2] = (float)z * 0.042;

  Serial.print("X:"); Serial.print(mag[0]); Serial.print(","); 
  Serial.print("Y:"); Serial.print(mag[1]); Serial.print(","); 
  Serial.print("Z:"); Serial.print(mag[2]); Serial.println("");
  JsonSet();
}
/* JSONデータの生成 */
void JsonSet(void){
  json = "{";
  json += "\"x\":" + String(mag[0])  + ",";
  json += "\"y\":" + String(mag[1]) + ",";
  json += "\"z\":" + String(mag[2]);
  json += "}";
}

/* コマンド送出 */
bool BM1422Cmd(uint8_t* cmd, uint8_t len){
  bool ret = false;

  Wire.beginTransmission(BM1422_ADRS); //スレーブが存在するか確認
  byte error = Wire.endTransmission();
  if( error == 0){ //スレーブが存在する場合下の処理
    Wire.beginTransmission(BM1422_ADRS);

    for(uint8_t i= 0; i < len; i++ ){
      Wire.write(*cmd);
      ++cmd;
    }
  
    Wire.endTransmission(); //ストップ・コンディションの発行
    ret = true;
  }

  return ret;
}
/* データを取得 */
bool BM1422GetData(uint8_t adr, uint8_t* reg_data, uint16_t len){
  bool  ret = false;

  Wire.beginTransmission(BM1422_ADRS); //スタート・コンディションの発行
  Wire.write(adr); //書き込む対象のアドレスをセット(ライトで指定)
  byte error = Wire.endTransmission();
  if( error == 0){ //スレーブが存在する場合下の処理
    if( Wire.requestFrom(BM1422_ADRS, len) == len ){
      for( uint16_t i=0; i < len; i++ ){
        *reg_data = Wire.read(); //len分だけデータをリードする
        ++reg_data;
      }
      ret = true;
    }
  }
  return ret;
}

htmlファイル(sdwifi.txt)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>3軸測定値</title>
</head>
<body>

<h1>現在の測定値</h1>

<div>X軸: <span id="x">---</span></div>
<div>Y軸: <span id="y">---</span></div>
<div>Z軸: <span id="z">---</span></div>

<script src="js.txt"></script>

</body>
</html>

JavaScriptファイル(js.txt)

async function update() {
  try {
    const r = await fetch("/data");
    const j = await r.json();

    document.getElementById("x").textContent = j.x;
    document.getElementById("y").textContent = j.y;
    document.getElementById("z").textContent = j.z;

  } catch(e) {
    document.getElementById("x").textContent = "ERR";
    document.getElementById("y").textContent = "ERR";
    document.getElementById("z").textContent = "ERR";
  }
}

setInterval(update, 1000);
update();

ソースコードの内容をコピーしてファイルをSDカードに作成指定使用してください。ファイル名は任意でもよいですが、8.3形式のファイル名にする必要があります。

関連リンク

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

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

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

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

Raspberry Pi Picoで学べるソフト開発

PR:外資系・IT業界などハイクラスの転職に強い【AXIS Agent】

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

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