ESP32で開発する環境を整える。

開発環境のインストール

  • Arduino IDE
    • ボードマネージャ(「ファイル」→「環境設定」→「追加のボードマネージャーのURL」)
      • ESP-WROOM-32
        • https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
        • esp32 > ESP32 Wrover Module or ESP32 Dev Module
      • ESP-WROOM-02(ESP8266)
        • http://arduino.esp8266.com/stable/package_esp8266com_index.json
        • esp8266
        • Generic ESP8266 Module
    • モジュール
      • matter(iOS『ホーム』)、esp32-alexa-wemo-emulator(Alexa)
  • Linuxの場合、usbserial.koとch341.koなどが必要。(/dev/ttyUSB0)

スケッチ

// Copyright © 2024 Yuichiro Nakada
#ifdef ESP8266
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#define WebServer ESP8266WebServer
// https://qiita.com/daidai100/items/ce7f05f20498e5aa8873 (自作ESP32簡易ボード)
#else
#include <WiFi.h>
#include <WebServer.h>
#endif

/*Put your SSID & Password*/
const char* ssid = " YourNetworkName";  // Enter SSID here
const char* password = " YourPassword";  //Enter Password here

WebServer server(80);

#define MAXPINS	17
uint8_t pin[MAXPINS];
uint8_t status[MAXPINS] = {
0xff, // 0: Don't use!
0xff, // 1: No Digital Output
0xff, // 2: Don't use!
0xff, // 3: Nothing??
0x00, // 4
0x00, // 5
0xff, // 6: Nothing??
0xff, // 7: Nothing??
0xff, // 8: Nothing??
0xff, // 9: Nothing??
0xff, // 10: Nothing??
0xff, // 11: Nothing??
0x00, // 12
0x00, // 13
0x00, // 14
0xff, // 15: Don't use!
0x00, // 16
};

void setup()
{
	Serial.begin(115200);
	delay(100);
	for (int i=0; i<MAXPINS; i++) {
		pin[i] = i;
		if (status[i]!=0xff) pinMode(pin[i], OUTPUT);
	}

	Serial.println("Connecting to ");
	Serial.println(ssid);

	// connect to your local wi-fi network
	WiFi.begin(ssid, password);

	// check wi-fi is connected to wi-fi network
	int n = 0;
	while (WiFi.status() != WL_CONNECTED) {
		delay(1000);
		Serial.print(".");
		n++;
		if (n>30) {
			// server mode
			Serial.println("");
			byte mac[6];
			WiFi.macAddress(mac);
			String ssid = "";
			for (int i=0; i<6; i++) {
				ssid += String(mac[i], HEX);
			}
			Serial.println("SSID: " + ssid);
			Serial.println("PASS: " + pass);
		}
	}

	Serial.println("");
	Serial.println("WiFi connected..!");
	Serial.print("Got IP: "); Serial.println(WiFi.localIP());

	server.on("/", handle_OnConnect);
	server.on("/on", handle_on);
	server.on("/off", handle_off);
	server.onNotFound(handle_NotFound);

	server.begin();
	Serial.println("HTTP server started");
}

void loop()
{
	server.handleClient();

	for (int i=0; i<MAXPINS; i++) {
		if (status[i]==0xff) continue;
		if (status[i]) digitalWrite(pin[i], HIGH);
		else digitalWrite(pin[i], LOW);
	}
}

void handle_OnConnect()
{
	for (int i=0; i<MAXPINS; i++) {
		if (status[i]!=0xff) status[i] = LOW;
	}
	server.send(200, "text/html", SendHTML());
}

void handle_on()
{
	int n = server.arg("id").toInt();
	status[n] = HIGH;
	Serial.println("GPIO"+String(n)+" Status: ON");
	server.send(200, "text/html", SendHTML()); 
}

void handle_off()
{
	int n = server.arg("id").toInt();
	status[n] = LOW;
	Serial.println("GPIO"+String(n)+" Status: OFF");
	server.send(200, "text/html", SendHTML()); 
}

void handle_NotFound()
{
	server.send(404, "text/plain", "Not found");
}

String SendHTML()
{
	String ptr = R"(<!DOCTYPE html> <html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <title>ESP32 Control</title>
    <style>
      html {font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}
      body {margin-top: 50px;}
      h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}
      p {font-size: 14px;color: #888;margin-bottom: 10px;}
      .button {display: block;width: 80px;background-color: #3498db;border: none;color: white;padding: 13px 30px;text-decoration: none;font-size: 25px;margin: 0px auto 35px;cursor: pointer;border-radius: 4px;}
      .button-on {background-color: #3498db;}
      .button-on:active {background-color: #2980b9;}
      .button-off {background-color: #34495e;}
      .button-off:active {background-color: #2c3e50;}
      .switch {position: relative; display: inline-block; width: 120px; height: 68px} 
      .switch input {display: none}
      .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px}
      .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
      input:checked+.slider {background-color: #2196F3}
      input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
      /*.slider { -webkit-appearance: none; margin: 14px; width: 360px; height: 25px; background: #FFD65C; outline: none; -webkit-transition: .2s; transition: opacity .2s;}
      .slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
      .slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; }*/
    </style>
  </head>
  <body>
    <h1>ESP32 Web Server</h1>
    <h3>Using Station(STA) Mode</h3>
    <!--%BUTTONPLACEHOLDER%-->

    <script>
function toggleCheckbox(element) {
  var xhr = new XMLHttpRequest();
  if (element.checked){ xhr.open("GET", "/update?state=1", true); }
  else { xhr.open("GET", "/update?state=0", true); }
  xhr.send();
}
setInterval(function() {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var inputChecked;
      var outputStateM;
      if (this.responseText == 1) { 
        inputChecked = true;
        outputStateM = "On";
      } else { 
        inputChecked = false;
        outputStateM = "Off";
      }
      document.getElementById("output").checked = inputChecked;
      document.getElementById("outputState").innerHTML = outputStateM;
    }
  };
  xhttp.open("GET", "/state", true);
  xhttp.send();
}, 1000);
    </script>
    )";

	for (int i=0; i<MAXPINS; i++) {
		if (status[i]==0xff) continue;
		if (status[i]) {
			ptr +="<p>GPIO" + String(i) + " Status: ON</p><a class=\"button button-off\" href=\"/off?id=" + String(i) + "\">OFF</a>\n";
		} else {
			ptr +="<p>GPIO" + String(i) + " Status: OFF</p><a class=\"button button-on\" href=\"/on?id=" + String(i) + "\">ON</a>\n";
		}
	}

	ptr += "</body>\n";
	ptr += "</html>\n";
	return ptr;
}
  • esp8266.ino (ESPAsyncWebServer)
// Copyright © 2024 Yuichiro Nakada
#ifdef ESP32
  #include <WiFi.h>
  #include <ESPmDNS.h>
  //#include <WebServer.h>
  #include <AsyncTCP.h>
#else // ESP8266
  #include <ESP8266WiFi.h>
  #include <ESP8266mDNS.h>
  //#include <ESP8266WebServer.h>
  //#define WebServer ESP8266WebServer
  #include <ESPAsyncTCP.h>
  // https://qiita.com/daidai100/items/ce7f05f20498e5aa8873 (自作ESP32簡易ボード)
#endif
// need to edit direct at ESPAsyncWebServer/src/WebResponseImpl.h
//#define TEMPLATE_PLACEHOLDER '$'
#include <ESPAsyncWebServer.h>

#define SSID		"ssid"		// Enter SSID here
#define PASSWORD	"password"	// Enter Password here
#define PASS		"pass"		// for server mode
#if __has_include("secret.h")
#include "secret.h"
#endif

AsyncWebServer server(80);

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ESP32 Control</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
    <style>
/* https://pote-chil.com/css-stock/ja */
.heading-40 {
    padding: .5em .7em;
    border-top: 2px solid #5ba9f7;
    border-bottom: 2px solid #5ba9f7;
    background-image: linear-gradient(45deg, #5ba9f712 25%, transparent 25%, transparent 50%, #5ba9f712 50%, #5ba9f712 75%, transparent 75%, transparent), linear-gradient(-45deg, #5ba9f712 25%, transparent 25%, transparent 50%, #5ba9f712 50%, #5ba9f712 75%, transparent 75%, transparent);
    background-color: #5ba9f70d;
    background-size: 20px 20px;
    color: #5ba9f7;
}

.toggle-button-4 {
    display: flex;
    align-items: center;
    position: relative;
    width: 100px;
    height: 50px;
    border-radius: 50px;
    box-sizing: content-box;
    background-color: #ff8d8d33;
    cursor: pointer;
    transition: background-color .4s;
}
.toggle-button-4:has(:checked) {
    background-color: #75bbff33;
}
.toggle-button-4::before {
    position: absolute;
    left: 5px;
    width: 42px;
    height: 42px;
    border-radius: 50%;
    background-color: #ff8d8d;
    content: '';
    transition: left .4s;
}
.toggle-button-4:has(:checked)::before {
    left: 50px;
    background-color: #75bbff;
}
.toggle-button-4::after {
    position: absolute;
    left: 26px;
    transform: translateX(-50%);
    color: #fff;
    font-weight: 600;
    font-size: .9em;
    content: 'OFF';
    transition: left .4s;
}
.toggle-button-4:has(:checked)::after {
    left: 71px;
    content: 'ON';
}
.toggle-button-4 input {
    display: none;
}

/* https://catnose.me/learning/html/input-range */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer; /* カーソルを分かりやすく */
  outline: none; /* スライダーのアウトラインは見た目がキツイので消す */
  height: 14px; /* バーの高さ */
  width: 100%; /* バーの幅 */
  background: #8acdff; /* バーの背景色 */
  border-radius: 10px; /* バーの両端の丸み */
  border: solid 3px #dff1ff; /* バー周囲の線 */
}
/* WebKit向けのつまみ */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; /*  🚩デフォルトのつまみのスタイルを解除 */
  background: #53aeff; /* 背景色 */
  width: 24px; /* 幅 */
  height: 24px; /* 高さ */
  border-radius: 50%; /* 円形に */
  box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.15); /* 影 */
}
/* Moz向けのつまみ */
input[type="range"]::-moz-range-thumb {
  background: #53aeff; /* 背景色 */
  width: 24px; /* 幅 */
  height: 24px; /* 高さ */
  border-radius: 50%; /* 円形に */
  box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.15); /* 影 */
  border: none; /* デフォルトの線を消す */
}
/* Firefoxで点線が周りに表示されてしまう問題の解消 */
input[type="range"]::-moz-focus-outer {
  border: 0;
}
/* つまみをドラッグしているときのスタイル */
input[type="range"]:active::-webkit-slider-thumb {
  box-shadow: 0px 5px 10px -2px rgba(0, 0, 0, 0.3);
}
    </style>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <h1 class="title">
        ESP32 Web Server
      </h1>
      <p class="subtitle">
        Using <strong>Station(STA)</strong> Mode!
      </p>

    $PLACEHOLDER$

    </div>
  </section>
  <script>
function toggleCheckbox(element, id) {
  var xhr = new XMLHttpRequest();
  if (element.checked) { xhr.open("GET", "/update?id="+id+"&state=1", true); }
  else { xhr.open("GET", "/update?id="+id+"&state=0", true); }
  xhr.send();
}
setInterval(function() {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      let array = this.responseText.split(",");
      for (let i=0; i<array.length; i+=2) {
        let inputChecked;
        let outputStateM;
        if (array[i+1] == 1) {
          inputChecked = true;
          outputStateM = "On";
        } else {
          inputChecked = false;
          outputStateM = "Off";
        }
        document.getElementById("check"+array[i]).checked = inputChecked;
        document.getElementById("outputState"+array[i]).innerHTML = outputStateM;
      }
    }
  };
  xhttp.open("GET", "/state", true);
  xhttp.send();
}, 1000);
  </script>
  </body>
</html>
)rawliteral";

#define MAXPINS	17
uint8_t status[MAXPINS] = {
0xff, // 0: Don't use!
0xff, // 1: No Digital Output
0x00, // 2: LED
0xff, // 3: Nothing??
0x00, // 4
0x00, // 5
0xff, // 6: Nothing??
0xff, // 7: Nothing??
0xff, // 8: Nothing??
0xff, // 9: Nothing??
0xff, // 10: Nothing??
0xff, // 11: Nothing??
0x00, // 12
0x00, // 13
0x00, // 14
0xff, // 15: Don't use!
0x00, // 16
};

/*String getTemperature()
{
	float t = dht.readTemperature();
	if (isnan(t)) {    
		Serial.println("Failed to get temperature!");
		return "--";
	} else {
		Serial.println(t);
		return String(t) + " ℃";
	}
}
 
String getHumidity()
{
	float h = dht.readHumidity();
	if (isnan(h)) {
		Serial.println("Failed to get humidity!");
		return "--";
	} else {
		Serial.println(h);
		return String(h) + " %";
	}
}*/

String outputState(int n)
{
//	if (digitalRead(output)) return "checked";
	if (status[n]) return " checked";
	return "";
}

String processor(const String& var)
{
	if (var=="PLACEHOLDER") {
		String buttons = "";
		for (int i=0; i<MAXPINS; i++) {
			if (status[i]==0xff) continue;
			buttons += "GPIO #" +String(i)+ "<div class='columns'><div class='column'><div class='select'><select><option>Digital Output</option><option>PWM</option><option>Digital Input</option><option>Analog Input</option></select></div></div><div class='column'><label class='toggle-button-4'><input type='checkbox' onchange=\"toggleCheckbox(this, " +String(i)+ ")\" id=\"check\"" +outputState(i)+ "></label></div></div>\n";
		}
		return buttons;
	}
	/*else if (var=="TEMPERATURE") {
		return getTemperature();
	} else if (var=="HUMIDITY") {
		return getHumidity();
	}*/
	return "<"+var+">";
}

void loop()
{
}

void setup()
{
	Serial.begin(115200);
	delay(100);
	for (int i=0; i<MAXPINS; i++) {
		if (status[i]!=0xff) pinMode(i, OUTPUT);
	}

	Serial.println("Connecting to ");
	Serial.println(SSID);

	// connect to your local wi-fi network
	WiFi.begin(SSID, PASSWORD);

	// check wi-fi is connected to wi-fi network
	int n = 0;
	while (WiFi.status() != WL_CONNECTED) {
		delay(1000);
		Serial.print(".");
		n++;
		if (n>30) {
			// server mode
			Serial.println("");
			byte mac[6];
			WiFi.macAddress(mac);
			String ssid = "";
			for (int i=0; i<6; i++) {
				ssid += String(mac[i], HEX);
			}
			Serial.println("SSID: " + ssid);
			Serial.println("PASS: " PASS);
		}
	}

	Serial.println("");
	Serial.println("WiFi connected");
	Serial.print("IP: "); Serial.println(WiFi.localIP());

	// Activate mDNS this is used to be able to connect to the server
	// with local DNS hostmane esp8266.local
	if (MDNS.begin("esp8266")) {
		Serial.println("MDNS responder started");
	}

	server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
		request->send_P(200, "text/html", index_html, processor);
	});
	/*server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request) {
		request->send_P(200, "text/plain", getTemperature().c_str());
	});
	server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request) {
		request->send_P(200, "text/plain", getHumidity().c_str());
	});*/

	// Send a GET request to <ESP_IP>/update?state=<inputMessage>
	server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
		String inputMessage;
		String inputParam;
		int n = request->getParam("id")->value().toInt();
		// GET input1 value on <ESP_IP>/update?state=<inputMessage>
		if (request->hasParam("state")) {
			inputMessage = request->getParam("state")->value();
			inputParam = "state";
			status[n] = inputMessage.toInt();
			if (status[n]) digitalWrite(n, HIGH);
			else digitalWrite(n, LOW);
		} else {
			inputMessage = "No message sent";
			inputParam = "none";
		}
		Serial.println(inputParam +String(n)+ ":" + inputMessage);
		request->send(200, "text/plain", "OK");
	});
	server.on("/state", HTTP_GET, [] (AsyncWebServerRequest *request) {
//		request->send(200, "text/plain", String(digitalRead(output)).c_str());
		request->send(200, "text/plain", ("5,"+String(status[5])).c_str());
	});

	server.begin();
	Serial.println("HTTP server started");
}