0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

『キャプティブポータル』の説明は省略しますが、
スマホでWifiに接続すると、特定のURLをアクセスして『インターネットへの接続』を確認するそうです。


このメカニズムを利用して、キャプティブポータル で遊ぼうと思います。

  • ESP32-C3をAPモードでWifi接続します
  • 次に、スマホのWifi画面からSSID『ESP32-C3_AP』に接続します
    • すると、スマホはバックグラウンドで前記URLにアクセスします
    • ESP32はDNSサーバーを偽装し、すべてに『自身』のアドレスを応答します
    • スマホからのHTTP-GETに対し、用意していおいたHTMLを応答します
  • 少し待っていると、用意していおいたHTMLが表示されます

WiFiに接続すると、待っているだけで キャプティブポータル が勝手に表示されます。


CircuitPythonだと、次のコードで実現できます。

CircuitPythonには、DNSライブラリが用意されいないため、少しコードが長くなってしまいます。

import socketpool
import wifi
from adafruit_httpserver import Server, Response, GET

# 1. APモード(アクセスポイント)の起動
SSID = "ESP32-C3_AP"

print("Starting Access Point...")
wifi.radio.start_ap(ssid=SSID)
print(f"AP Started! IP Address: {wifi.radio.ipv4_address_ap}")

pool = socketpool.SocketPool(wifi.radio)
server = Server(pool)

# スマホに表示させるHTML画面
HTML_PAGE = """<!DOCTYPE html><html><head><meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>ESP32-C3 Notification</title>
<style>body{font-family:sans-serif; text-align:center; padding:50px; background:#f0f2f5;}
h1{color:#ff5722;} p{font-size:18px; color:#333;}</style></head>
<body>
<h1>🔔 キャプティブポータルで遊ぶ (CircuitPython)</h1>
<p>ESP32-C3への接続が完了しました。</p>
<p>最新のライブラリ環境でキャプティブポータルが動作しています。</p>
</body></html>"""

# メイン画面のルーティング
@server.route("/", GET)
def base(request):
    return Response(request, content_type="text/html", body=HTML_PAGE)

# 接続検知ルーティング
# すべてのアクセスを強制フックするルーティング 
@server.route("/...", GET)
def apple_captive(request):
    return Response(request, content_type="text/html", body=HTML_PAGE)

# サーバーの開始
server.start(host=str(wifi.radio.ipv4_address_ap), port=80)

# 簡易DNSサーバー(UDP 53番ポート)の設定
dns_socket = pool.socket(pool.AF_INET, pool.SOCK_DGRAM)
dns_socket.bind((str(wifi.radio.ipv4_address_ap), 53))
dns_socket.setblocking(False)

print("Captive Portal is ready.")
ip_bytes = bytes(int(x) for x in str(wifi.radio.ipv4_address_ap).split("."))
dns_buffer = bytearray(512)

while True:
    # 簡易DNSサーバー処理
    try:
        bytes_received, addr = dns_socket.recvfrom_into(dns_buffer)
        if bytes_received > 0:
            data = dns_buffer[:bytes_received]
            
            # DNSの標準的な応答パケットを生成
            packet = data[:2] + b"\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00"
            idx = 12

            while data[idx] != 0:
                idx += data[idx] + 1
            idx += 5
            
            packet += data[12:idx]
            # 回答レコード(TTL: 60秒、データ長: 4バイト、ESP32のIPアドレス)
            packet += b"\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04" + ip_bytes
            
            dns_socket.sendto(packet, addr)
    except OSError:
        pass

    # Webサーバー処理
    try:
        server.poll()
    except OSError:
        pass

上記コードにて、Androidスマホに ↓このようなHTMLが表示されます。

android.jpg




もう少し遊ぶため、NeoPixel を 光らせてみたいと思います。
fetchを使いますが、CircuitPythonだとレスポンスが追いつきませんので、Arduinoで実装します。


環境は次の通り;
(必要によりダウンロードして、Arduino-IDE に ZIPインストールしてください)



コードは以下の通り。

fullcolor.ino
#include <Arduino.h>
#include <WiFi.h>
#include <DNSServer.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_NeoPixel.h>

#define PIN_NEOPIXEL 4
#define NUM_PIXELS   1

const char* ssid = "ESP32_NeoPixel_AP";

Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
AsyncWebServer server(80);
DNSServer dnsServer;

// グローバル変数で現在の色を記憶(明るさ変更時に元の色を保つため)
uint8_t currentR = 0;
uint8_t currentG = 0;
uint8_t currentB = 0;

// HTML画面 (ダークモード)
const char HTML_PAGE[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html><head><meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>NeoPixel Controller</title>
<style>
  body { font-family: -apple-system, sans-serif; text-align: center; padding: 20px; background: #1c1c1e; color: #ffffff; }
  h1 { color: #ffffff; font-size: 24px; margin-bottom: 20px; font-weight: 600; }
  .card { background: #2c2c2e; padding: 25px; border-radius: 20px; display: inline-block; box-shadow: 0 4px 16px rgba(0,0,0,0.4); width: 85%; max-width: 320px; margin-bottom: 20px; }
  input[type="color"] { -webkit-appearance: none; border: none; width: 130px; height: 130px; border-radius: 50%; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.5); margin: 10px 0; background: none; }
  input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
  input[type="color"]::-webkit-color-swatch { border: 2px solid #3a3a3c; border-radius: 50%; }
  
  .slider-container { text-align: left; margin-top: 20px; }
  .slider-label { font-size: 14px; color: #aeaeActive; color: #aeaeb2; margin-bottom: 8px; font-weight: 500; display: flex; justify-content: space-between; }
  input[type="range"] { -webkit-appearance: none; width: 100%; height: 8px; border-radius: 4px; background: #3a3a3c; outline: none; }
  input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 28px; height: 28px; border-radius: 50%; background: #ffffff; box-shadow: 0 2px 6px rgba(0,0,0,0.5); cursor: pointer; }
</style></head>
<body>
  <h1>🌈 フルカラーで遊ぶ</h1>
  
  <div class="card">
    <p style="margin:0 0 10px 0; color:#f2f2f7; font-weight:600;">カラー選択</p>
    <input type="color" id="colorPicker" value="#000000">
    
    <div class="slider-container">
      <div class="slider-label">
        <span>明るさ</span>
        <span id="brightVal">50%</span>
      </div>
      <input type="range" id="brightSlider" min="0" max="100" value="50">
    </div>
  </div>

  <script>
    const picker = document.getElementById('colorPicker');
    const slider = document.getElementById('brightSlider');
    const brightVal = document.getElementById('brightVal');
    
    picker.addEventListener('input', (e) => {
      const hex = e.target.value;
      const r = parseInt(hex.substring(1, 3), 16);
      const g = parseInt(hex.substring(3, 5), 16);
      const b = parseInt(hex.substring(5, 7), 16);
      fetch(`/set_color?r=${r}&g=${g}&b=${b}`).catch(err => {});
    });
    
    slider.addEventListener('input', (e) => {
      const val = e.target.value;
      brightVal.innerText = val + '%';
      fetch(`/set_brightness?b=${val}`).catch(err => {});
    });
  </script>
</body></html>
)rawliteral";

void setup() {
  Serial.begin(115200);
  delay(10);

  pixels.begin();
  pixels.setBrightness(128); // 💡 デフォルト50% (255の約半分)
  pixels.setPixelColor(0, pixels.Color(0, 0, 0));
  pixels.show();

  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid);
  IPAddress apIP = WiFi.softAPIP();
  Serial.print("AP IP Address: ");
  Serial.println(apIP);

  dnsServer.start(53, "*", apIP);

  // カラーデータ受信エンドポイント
  server.on("/set_color", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request->hasParam("r") && request->hasParam("g") && request->hasParam("b")) {
      currentR = request->getParam("r")->value().toInt();
      currentG = request->getParam("g")->value().toInt();
      currentB = request->getParam("b")->value().toInt();
      
      pixels.setPixelColor(0, pixels.Color(currentR, currentG, currentB));
      pixels.show();
    }
    request->send(200, "text/plain", "OK");
  });

  // 明るさデータ受信エンドポイント
  server.on("/set_brightness", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request->hasParam("b")) {
      int brightPercent = request->getParam("b")->value().toInt();
      // 0-100% の値を 0-255 の輝度値に変換
      uint8_t brightDuty = (brightPercent * 255) / 100;
      
      pixels.setBrightness(brightDuty);
      // 明るさ変更を即座に反映させるため、現在の色で再描画
      pixels.setPixelColor(0, pixels.Color(currentR, currentG, currentB));
      pixels.show();
    }
    request->send(200, "text/plain", "OK");
  });

  // ワイルドカードによる全アクセス強制キャッチ
  server.on("*", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/html", HTML_PAGE);
  });
  
  server.onNotFound([](AsyncWebServerRequest *request){
    request->send(200, "text/html", HTML_PAGE);
  });

  server.begin();
  Serial.println("System Ready.");
}

void loop() {
  dnsServer.processNextRequest();
  delay(5);
}

iPhoneでは、下記動画のようにリアルタイムで色が変わって遊べます。

fullcolor.gif
↑(スロー動画ではありません)動きが鈍いときは画像をクリック

OS標準のカラーピッカーが使われますが、自分の古いAndroidスマホだと、リアルタイムで色を切り替えられませんでした。


キャプティブポータルを作れると、いつか何かで役に立つ場面があるように思います。

以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?