前書き的なもの
この度ESP32シリーズでENC28J60、W5100、W5500を使いHTTP通信を試す機会があったものの、分かり易いサンプルや説明がすぐに見つから無かったので備忘録的にまとめておきました。
マイコン用イーサネットボードについて
ENC28J60、W5100、W5500等のイーサネットボードは主にTCP/IPまでをサポートするハードウエアで、イーサネットボードに搭載されているRJ45(LAN)コネクタにて外部ネットワーク機器との通信を肩代わりしてマイコン側でシリアル制御しつつ通信内容のデータを処理する物となっています。
そのため、基本的にTCP/IP(OSI参照モデルの第4層:トランスポート層)以上の事は各々が利用したいプロトコルを理解し自分で処理を記述して利用する形となります。
正確には搭載チップ的にOSI参照モデルの下層も制御できますが、ほとんどの用途では必要ないと思いますし、制御するにしてもEthernetライブラリ等に頼らず自力でマニュアルを見ながらイーサネットボードを制御する処理を作成することになると思います。
HTTPを利用する為の前程知識
マイコンにアクセスしてとりあえずページを表示させたい場合は、サンプルコードまで読み飛ばしてもらって構いません。
但しURLのアクセスディレクトリ毎に処理や表示を分けたい場合や、マイコン側にデータを送信してそれに応じた処理を行わせたい等の場合は、HTTPを利用する上で「リクエスト」と「レスポンス」を理解する必要があります。
リクエスト
「リクエスト」とはクライアント(Webブラウザ等)からのアクセスで、以下のような形式をしています。
GET / HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: ja
Cache-Control: no-cache
Connection: keep-alive
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 >(KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
この中で一番大事なのが一行目のリクエスト行と言われるものです。
リクエスト行以降はクライアントに関する情報やアクセス制御、POST等データ送信などでリクエスト本文がある場合の本文のデータ種別に関する情報が記載された「リクエストヘッダー」、データの送信時にデータを記載する本文である「リクエストボディ」が続きます。
リクエスト行
GET / HTTP/1.1
リクエスト行は
メソッド ディレクトリ HTTPバージョン
で構成されており、各要素の間には半角のスペースが入っています。
この中でHTTP通信を処理する上で「メソッド」、「ディレクトリ」の2つは理解する必要があります。
メソッド
メソッドとは主に「GET」、「POST」と言われるもので、大雑把に
GET -> ページ等の表示を伴うデータの送受信
POST -> データの送受信のみ
のような感じで捉えておくといいです。
ブラウザでページにアクセスする際も「GET」にてページデータへのアクセス要求がされ、サーバー側からページデータのHTML等を返答する形になります。
この説明は大雑把になんとなく理解してもらうためなので、ちゃんと理解したい方やこれから先HTTPに関して勉強したい方、詳しく知りたい方は他の記事や書籍を参照してください。
ディレクトリ
ディレクトリとはURLにおいてドメインの後に続くものです。
上記のURLで「 http://example.com 」の後に続く「/mydir/」がディレクトリとなり、このURLにGETリクエストがあった場合は以下のようになります。
GET /mydir/ HTTP/1.1
また、ブラウザなどのフォームからGETにてデータが送信されてきた場合は以下のように「?」に続いてデータが添付されています。
GET /?hoge=1 HTTP/1.1
データは「名前=中身」の形になっており、この場合「hoge」という名前のデータが「1」という形になります。
さらにGETにて複数のデータが添付されてきた場合は以下のように「&」で区切る形で繋がってデータが送信されます。
GET /?hoge=1&hogehoge=2 HTTP/1.1
この場合、「hoge」という名前のデータが「1」、「hogehoge」という名前のデータが「2」となります。
リクエストヘッダー
Host: example.com
リクエストヘッダー内で主に利用される物としてはホスト名があります。
ここにはアクセスされた際のドメイン名が記載されており、IPアドレスで直接アクセスした際はそのIPアドレスが表示されます。
その他POST等で本文に何かしらのデータが存在する場合は、
Content-Type: application/json
等で本文のデータ種別が記載されています。
リクエストボディ
リクエスト行、リクエストヘッダーに続いてPOST等でデータが送信されてきた際には空白行を挟んでリクエストボディが存在します。
POST / HTTP/1.1
Content-Type: application/json
Content-Length: 17
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 >(KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36hoge=1&hogehoge=2
リクエストヘッダーは端末などによって項目や長さが変わるため、リクエストボディとの間には1行の空白行が存在します。
また、リクエストボディが存在する場合はリクエストヘッダー内に「Content-Length: 17」の様な項目が存在し、データの長さ(文字数等)が記載されます。
リクエストボディのデータも「名前=中身」の形で送信され、複数存在する場合は「&」で区切られて繋がっています。
レスポンス
クライアント(Webブラウザ等)からの要求に対する返答です。
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Hello ESP32 Ethernet</title>
</head>
<body>
<h2>Hello ESP32 Ethernet</h2>
</body>
</html>
今回はマイコンで利用する上での最低限のみ記載しています。
中身はリクエストと同じような構成となっており、一行目からリクエストに対する「レスポンス行」、レスポンス本文のデータ形式や通信制御などを記載している「レスポンスヘッダー」、一行の空白行を挟んで本文の「レスポンスボディ」です。
レスポンス行
HTTP/1.1 200 OK
レスポンス行は
HTTPバージョン HTTPステータスコード
で構成されており、こちらも各要素の間には半角のスペースが入っています。
HTTPバージョンは自身のプログラムにて対応しているバージョンを記載してしまって構いません。
HTTPステータスコードに関しては「200 OK」までが一つのステータスコードとなっており、「数字のステータスコード メッセージ」で構成されています。
こちらは状況に応じて使い分けた方が良いので、詳しくは「HTTPステータスコード」で調べてみてください。
マイコンで簡単に利用する分には
200 OK
400 Bad Request
404 Not Found
500 Internal Server Error
の4つ程度を覚えておけば問題ないかと思います。
各HTTPステータスコードの簡単な説明としては以下となります。
200 :リクエストを正常に処理
400 :クライアントエラー、リクエストの処理不可
404 :ページが見つからない
500 :何かしらのサーバエラー
レスポンスヘッダー
Content-Type: text/html
今回のレスポンス例文では、上記のようにレスポンス本文のデータ種別をMIMEタイプと呼ばれる構文で記載しています。
このようにレスポンスの本文は「textでありhtml構文」だと教えてあげることで、webブラウザ等はHTML文として解釈してHTMLタグに基づきページを描写してくれます。
Content-Type: text/plain
仮に上記のようにContent-Typeを「text/plain」に変更した場合は、レスポンス本文をHTMLとは解釈されずそのままtextとして表示します。
それ以外にサーバー側からHTTPの通信制御を行いたい場合もここに追加で設定を記載します。
例えば、一度データの返答を行った後にしばらく再度通信の可能性があり接続を維持しておきたい場合は
Connection: Keep-Alive
Keep-Alive: timeout=5, max=999
をヘッダーへ加えたりします。
Connectionの「Keep-Alive」とは「接続を維持」の意味となります。
二行目の「Keep-Alive」では接続を維持する条件を設定しており、「timeout=5」で「5秒通信が無ければ切断」、「max=999」で「999回リクエスト送信で切断」となります。
ESP32とイーサネットボードによるHTTPサーバーのサンプル
以下のサンプルはESP32シリーズ向けの為、ESP32内蔵のEthernet用MACアドレスを読みだしてイーサネットボードに設定するようにしてあります。
ESP32以外で使用する際は
#include "esp_system.h"
esp_read_mac(mac, ESP_MAC_ETH);
の二つをコメントアウトか削除してください。
また、使用するイーサネットボードに応じてincludeされているヘッダーファイルを適時変更してください。
例: W5100、W5500を使用する場合
#include <SPI.h>
//#include <UIPEthernet.h> //ENC28J60
#include <Ethernet.h> //W5100、W5500
#include "esp_system.h"
~~~ 以下略 ~~~
SPIの各PINを定義してある部分に関しては、使用しているマイコンボードに合わせて適時変更してください。
#define PIN_CS 17 //CS
#define PIN_MOSI 23 //MOSI
#define PIN_MISO 19 //MISO
#define PIN_SCK 18 //SCK
ページを表示するだけのサンプル
#include <SPI.h>
#include <UIPEthernet.h> //ENC28J60
//#include <Ethernet.h> //W5100、W5500
#include "esp_system.h"
#define PIN_CS 17 //CS
#define PIN_MOSI 23 //MOSI
#define PIN_MISO 19 //MISO
#define PIN_SCK 18 //SCK
byte mac[6] = {0xa1,0xb2,0xc3,0xd4,0xe5,0xf6};
IPAddress myIP(192, 168, 1, 200);
IPAddress myMASK(255, 255, 255, 0);
IPAddress myDNS(192, 168, 1, 1);
IPAddress myGW(192, 168, 1, 1);
EthernetServer server(HTTP_PORT);
void setup(){
Serial.begin(115200);
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CS);
Ethernet.init(PIN_CS);
esp_read_mac(mac, ESP_MAC_ETH);
Serial.printf("[Ethernet] Mac : %02X:%02X:%02X:%02X:%02X:%02X\r\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Ethernet.begin(mac);
//Ethernet.begin(mac,myIP);
if(Ethernet.hardwareStatus() == EthernetNoHardware){
Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :(");
while(true){
delay(1);
}
}
Serial.println("ok....");
Serial.print("localIP: ");
Serial.println(Ethernet.localIP());
Serial.print("subnetMask: ");
Serial.println(Ethernet.subnetMask());
Serial.print("gatewayIP: ");
Serial.println(Ethernet.gatewayIP());
Serial.print("dnsServerIP: ");
Serial.println(Ethernet.dnsServerIP());
server.begin();
Serial.println("Server Start");
}
void loop()
{
EthernetClient client = server.available();
if(client){
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>Hello ESP32 Ethernet</title>\n</head>\n<body>\n<h2>Hello ESP32 Ethernet</h2>\n</body>\n</html>"));
}
client.stop();
}
上記サンプルでは
esp_read_mac(mac, ESP_MAC_ETH);
を利用してESP32から内蔵されているEthernet用Macアドレスを読みだして使用しています。
また、疎通確認の為のpingを行う場合は、loop関数冒頭の
EthernetClient client = server.available();
をloop関数内に記載するだけでpingへ応答します。
HTMLページのリクエストに対する返答処理は
HTTP/1.1 200 OK
Content-Type: text/html
から始まり、以降一行の空白行を挟んで本文になります。
1行目はHTTPのバージョンとリクエストが問題なく正常に返答するので「200 OK」となります。
2行目は返答する本文のデータ種別で、HTMLページを返答するので「Content-Type: text/html」となります。
これらをまとめ本文を付け加えると、上記サンプルの場合はアクセスしてきたクライアントへ以下の内容を返答します。
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Hello ESP32 Ethernet</title>
</head>
<body>
<h2>Hello ESP32 Ethernet</h2>
</body>
</html>
GETでアクセスの際にディレクトリで処理を分ける場合
#include <SPI.h>
#include <UIPEthernet.h> //ENC28J60
//#include <Ethernet.h> //W5100、W5500
#include "esp_system.h"
#define LED_PIN 5 //LED
#define PIN_CS 17 //CS
#define PIN_MOSI 23 //MOSI
#define PIN_MISO 19 //MISO
#define PIN_SCK 18 //SCK
#define REQ_BUF_SZ 120
#define HTTP_PORT 80
char HTTP_req[REQ_BUF_SZ] = {0};
char req_index = 0;
byte mac[6] = {0xa1,0xb2,0xc3,0xd4,0xe5,0xf6};
IPAddress myIP(192, 168, 1, 200);
IPAddress myMASK(255, 255, 255, 0);
IPAddress myDNS(192, 168, 1, 1);
IPAddress myGW(192, 168, 1, 1);
EthernetServer server(HTTP_PORT);
void setup(){
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CS);
Ethernet.init(PIN_CS);
esp_read_mac(mac, ESP_MAC_ETH);
Serial.printf("[Ethernet] Mac : %02X:%02X:%02X:%02X:%02X:%02X\r\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Ethernet.begin(mac);
if(Ethernet.hardwareStatus() == EthernetNoHardware){
Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :(");
while(true){
delay(1);
}
}
Serial.println("ok....");
Serial.print("localIP: ");
Serial.println(Ethernet.localIP());
Serial.print("subnetMask: ");
Serial.println(Ethernet.subnetMask());
Serial.print("gatewayIP: ");
Serial.println(Ethernet.gatewayIP());
Serial.print("dnsServerIP: ");
Serial.println(Ethernet.dnsServerIP());
server.begin();
Serial.println("Server Start");
}
void loop()
{
EthernetClient client = server.available();
if(client)
{
boolean swLED = false;
while(1){
char c = client.read();
if(req_index < (REQ_BUF_SZ - 1)){
HTTP_req[req_index] = c;
req_index++;
}
if(c == '\n'){
req_index = 0;
break;
}
}
if(HTTP_req[0] == 'G'&&
HTTP_req[1] == 'E'&&
HTTP_req[2] == 'T'){
req_index = 4;
if(HTTP_req[req_index+1] == 'L' &&
HTTP_req[req_index+2] == 'E' &&
HTTP_req[req_index+3] == 'D' &&
HTTP_req[req_index+4] == ' '){
swLED = true;
}
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
if(swLED){
digitalWrite(LED_PIN, HIGH);
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>Hello ESP32 Ethernet</title>\n</ head>\n<body>\n<h2>LED ON</h2>\n</body>\n</html>"));
}
else{
digitalWrite(LED_PIN, LOW);
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>Hello ESP32 Ethernet</title>\n</ head>\n<body>\n<h2>LED OFF</h2>\n</body>\n</html>"));
}
}
else{
client.println("HTTP/1.1 400 Bad Request");
}
req_index = 0;
while(1){
HTTP_req[req_index] = NULL;
req_index++;
if(req_index >= REQ_BUF_SZ) break;
}
req_index = 0;
}
client.stop();
}
このサンプルでは「 http://IPアドレス/ 」にブラウザからアクセスすると「LED OFF」と表示され、
「 http://IPアドレス/LED 」にアクセスした際だけ「LED ON」と表示されて
#define LED_PIN 5
で設定したピンに繋いだLEDが点灯します。
なお、「 http://IPアドレス/LED/ 」とURLの最後に「/」を付けた場合は「LED OFF」と表示されます。
また、GET以外のPOST等でアクセスした場合は400が返されます。
char c = client.read();
このサンプルではクライアントからのリクエストを1文字ずつ読み込んでリクエストのメソッドが「GET」であるか確認をし、その後ディレクトリが「LED」であるかの確認をして処理を分けています。
while(1){
char c = client.read();
if(req_index < (REQ_BUF_SZ - 1)){
HTTP_req[req_index] = c;
req_index++;
}
if(c == '\n'){
req_index = 0;
break;
}
}
今回リクエストで必要なのが1行目のリクエスト行だけなので、1行目の終わりの改行コードが来るまでをwhileで配列に代入して読み込んでいます。
http:/IPアドレス/LED
GET /LED HTTP/1.1
処理を分けたいURLとそのリクエストを上記とした場合
if(HTTP_req[0] == 'G'&&
HTTP_req[1] == 'E'&&
HTTP_req[2] == 'T'){
req_index = 4;
if(HTTP_req[req_index+1] == 'L' &&
HTTP_req[req_index+2] == 'E' &&
HTTP_req[req_index+3] == 'D' &&
HTTP_req[req_index+4] == ' '){
swLED = true;
}
~~~ 以下略 ~~~
}
今回は分かり易く配列を1文字ずつ比較しています。
HTTP_reqの配列で受け取ったリクエストは配列の先頭から3つ目までは「GET」が入っており、
その後空白で区切られてディレクトリである「/LED」が来てまた空白となり最後にHTTPバージョンと改行が入ります。
そのため配列の[0]~[2]までが「GET」であるか確認し、[3]に空白が来て[4]にディレクトリ最初の「/」が入るのが分かると思います。
その後、[5]~[7]に「LED」を確認して最後空白で「/LED」ディレクトリかを確認しています。
最後の空白確認をしない場合、「 http:/IPアドレス/LEDled 」や「 http:/IPアドレス/LED/LED 」等も「LED ON」で表示されてしまいます。
また逆に、今回はディレクトリ終わりの空白までで確認としているため「 http:/IPアドレス/LED/ 」へアクセスした場合は「LED OFF」が表示されます。
実用例 : GETでデータを受け取ってESP32を操作するHTTPリモートスイッチ
#include <SPI.h>
//#include <UIPEthernet.h> //ENC28J60
#include <Ethernet.h> //W5100、W5500
#include <string>
#include "esp_system.h"
using namespace std;
#define SWT_PIN_1 3 //Remote_Switch_PIN_1
#define SWT_PIN_2 4 //Remote_Switch_PIN_2
#define LED_PIN 5 //OnBoard_LED
#define PIN_CS 10 //CS
#define PIN_RST 2 //RESET
#define PIN_MOSI 8 //MOSI
#define PIN_MISO 1 //MISO
#define PIN_SCK 9 //SCK
#define REQ_BUF_SZ 120
#define SOCKET_PORT 80
char HTTP_req[REQ_BUF_SZ] = {0};
char req_index = 0;
byte mac[6] = {0xa8,0x20,0x66,0x28,0x20,0x00};
IPAddress myIP(192, 168, 1, 200);
IPAddress myMASK(255, 255, 255, 0);
IPAddress myDNS(192, 168, 1, 1);
IPAddress myGW(192, 168, 1, 1);
EthernetServer server(SOCKET_PORT);
void setup(){
Serial.begin(115200);
pinMode(SWT_PIN_1, OUTPUT);
pinMode(SWT_PIN_2, OUTPUT);
digitalWrite(SWT_PIN_1, HIGH);
digitalWrite(SWT_PIN_2, HIGH);
pinMode(PIN_RST, OUTPUT);
digitalWrite(PIN_RST, HIGH);
SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CS);
Ethernet.init(PIN_CS);
esp_read_mac(mac, ESP_MAC_ETH);
Serial.printf("[Ethernet] Mac : %02X:%02X:%02X:%02X:%02X:%02X\r\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
Ethernet.begin(mac);
if(Ethernet.hardwareStatus() == EthernetNoHardware){
Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :(");
while(true){
delay(1);
}
}
Serial.println("ok....");
Serial.print("localIP: ");
Serial.println(Ethernet.localIP());
Serial.print("subnetMask: ");
Serial.println(Ethernet.subnetMask());
Serial.print("gatewayIP: ");
Serial.println(Ethernet.gatewayIP());
Serial.print("dnsServerIP: ");
Serial.println(Ethernet.dnsServerIP());
if(Ethernet.linkStatus() == LinkON) {
digitalWrite(LED_PIN, HIGH);
}
else{
digitalWrite(LED_PIN, LOW);
}
server.begin();
Serial.println("Server Start");
}
void loop()
{
EthernetClient client = server.available();
if(client)
{
int8_t RPIrst = 0;
while(1){
char c = client.read();
if(req_index < (REQ_BUF_SZ - 1)){
HTTP_req[req_index] = c;
req_index++;
}
if(c == '\n'){
req_index = 0;
break;
}
}
string strHttpMethod(HTTP_req, 3);
if(strHttpMethod == "GET"){
string strHttpDir(HTTP_req, 4, 12);
if(strHttpDir == "/favicon.ico"){
client.println("HTTP/1.1 404 Not Found");
client.println("Content-Type: text/html");
client.println();
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>404 Not Found</title>\n</head>\n<body>\n<h1>Not Found</h1>\n</body>\n</html>"));
}
else{
if(strHttpDir == "/?restart=1 "){
RPIrst = 1;
}
else if(strHttpDir == "/?restart=2 "){
RPIrst = 2;
}
else if(strHttpDir == "/?restart=12"){
RPIrst = 12;
}
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
//client.println("Content-Type: text/plain"); //デバッグ用
client.println();
if(RPIrst==1){
digitalWrite(SWT_PIN_1, LOW);
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>ESP32 Remote Switch</title>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta http-equiv=\"refresh\" content=\"2;URL=/\">\n</head>\n<body>\n<div style=\"margin: 0 auto; padding: 1em; padding-top: 0; width: 91vw; max-width: 600px;\">\n<h2 style=\"margin: 0 auto; padding: 1em; text-align: center; line-height: 30px; background-color: black; color: white;\"\">Raspberry Pi 4</h2>\n<br>\n<h2 style=\"margin: 0 auto; text-align: center;\">Status</h2>\n<h3 style=\"margin: 0 auto; text-align: center; color:red;\">1: offline</h3>\n<h3 style=\"margin: 0 auto; text-align: center; color:green;\">2: online</h3>\n<br>\n</div>\n<p style=\"text-align: center;\">© MediaCreators</p>\n</body>\n</html>"));
}
else if(RPIrst==2){
digitalWrite(SWT_PIN_2, LOW);
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>ESP32 Remote Switch</title>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta http-equiv=\"refresh\" content=\"2;URL=/\">\n</head>\n<body>\n<div style=\"margin: 0 auto; padding: 1em; padding-top: 0; width: 91vw; max-width: 600px;\">\n<h2 style=\"margin: 0 auto; padding: 1em; text-align: center; line-height: 30px; background-color: black; color: white;\"\">Raspberry Pi 4</h2>\n<br>\n<h2 style=\"margin: 0 auto; text-align: center;\">Status</h2>\n<h3 style=\"margin: 0 auto; text-align: center; color:green;\">1: online</h3>\n<h3 style=\"margin: 0 auto; text-align: center; color:red;\">2: offline</h3>\n<br>\n</div>\n<p style=\"text-align: center;\">© MediaCreators</p>\n</body>\n</html>"));
}
else if(RPIrst==12){
digitalWrite(SWT_PIN_1, LOW);
digitalWrite(SWT_PIN_2, LOW);
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>ESP32 Remote Switch</title>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta http-equiv=\"refresh\" content=\"2;URL=/\">\n</head>\n<body>\n<div style=\"margin: 0 auto; padding: 1em; padding-top: 0; width: 91vw; max-width: 600px;\">\n<h2 style=\"margin: 0 auto; padding: 1em; text-align: center; line-height: 30px; background-color: black; color: white;\"\">Raspberry Pi 4</h2>\n<br>\n<h2 style=\"margin: 0 auto; text-align: center;\">Status</h2>\n<h3 style=\"margin: 0 auto; text-align: center; color:red;\">1: offline</h3>\n<h3 style=\"margin: 0 auto; text-align: center; color:red;\">2: offline</h3>\n<br>\n</div>\n<p style=\"text-align: center;\">© MediaCreators</p>\n</body>\n</html>"));
}
else{
digitalWrite(SWT_PIN_1, HIGH);
digitalWrite(SWT_PIN_2, HIGH);
//client.println(strHttpDir.c_str()); //デバッグ用
client.print(F("<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n<title>ESP32 Remote Switch</title>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n</head>\n<body>\n<div style=\"margin: 0 auto; padding: 1em; padding-top: 0; width: 91vw; max-width: 600px;\">\n<h2 style=\"margin: 0 auto; padding: 1em; text-align: center; line-height: 30px; background-color: black; color: white;\"\">Raspberry Pi 4</h2>\n<br>\n<h2 style=\"margin: 0 auto; text-align: center;\">Status</h2>\n<h3 style=\"margin: 0 auto; text-align: center; color:green;\">1: online</h3>\n<h3 style=\"margin: 0 auto; text-align: center; color:green;\">2: online</h3>\n<br>\n<button type=\"button\" onclick=\"location.href='/?restart=1'\" value=\"ReStart\" style=\"margin: 0 auto !important; width: 29vw !important; max-width: 196px !important; height: 3em !important;\">Restart 1</button>\n<button type=\"button\" onclick=\"location.href='/?restart=2'\" value=\"ReStart\" style=\"margin: 0 auto !important; width: 29vw !important; max-width: 196px !important; height: 3em !important;\">Restart 2</button>\n<button type=\"button\" onclick=\"location.href='/?restart=12'\" value=\"ReStart\" style=\"margin: 0 auto !important; width: 29vw !important; max-width: 196px !important; height: 3em !important;\">Restart 1&2</button>\n</div>\n<p style=\"text-align: center;\">© MediaCreators</p>\n</body>\n</html>"));
}
}
}
else{
client.println("HTTP/1.1 400 Bad Request");
}
}
client.stop();
req_index = 0;
while(1){
HTTP_req[req_index] = NULL;
req_index++;
if(req_index >= REQ_BUF_SZ) break;
}
req_index = 0;
if(Ethernet.linkStatus() == LinkON) {
digitalWrite(LED_PIN, HIGH);
}
else{
digitalWrite(LED_PIN, LOW);
}
}
このサンプルは「 #define SWT_PIN_1 」、「 #define SWT_PIN_2 」で定義したピンをHTTPを利用してブラウザからGETでデータを送信してリモート操作するものです。
現在このプログラムはサーバー利用しているRaspberryPi 2台が外部操作を受け付けなくなった際などの緊急時に、リレーを利用して外部から電源を遮断して強制再起動するために実利用しています。
コードの中身としては、これまでの例と違いリクエストのメソッドやディレクトリをstring型に変換することで「 strHttpMethod == "GET" 」のように等価演算子で文字列比較できるので汎用性があります。
また、型変換の際にstring型のコンストラクタを利用して文字の切り出しも行っています。
char HTTP_req[] = "GET /favicon.ico HTTP/1.1";
string strHttpMethod(HTTP_req, 3);
string オブジェクト名(char配列, 頭から切り出す文字数);
strHttpMethod => "GET"string strHttpDir(HTTP_req, 4, 12);
string オブジェクト名(char配列, 配列の切り出し開始位置, 切り出す文字数);
strHttpDir => "/favicon.ico"
GETリクエストの際に「 restart=1 」等のデータを送信することで、1ならPIN_1、2ならPIN_2を、12ならPIN_1+PIN_2をリレー駆動させて遮断しつつレスポンスを返答します。
この際にレスポンス本文のHTMLヘッダーへ
<meta http-equiv="refresh" content="2;URL=/">
というメタタグを追加することで2秒後に自動リロードして、PIN_1+PIN_2を復帰させています。
後書き的なもの
今まで散々サーバーを扱ってきたにもかかわらず、いざマイコンでHTTPを扱うと色々と忘れてることが多く日々多様なライブラリに助けられているなと実感しました。