Help us understand the problem. What is going on with this article?

BLEでESP32から接続するWiFiのSSID・パスワードを設定する

ESP32にWifiアクセスポイントに接続する能力があるのに、画面や入力手段がないと、SSIDやパスワードをファームウェアに埋め込むことになり、固定値となってしまいます。

世の中の記事を見ると、いったん自身がWifiアクセスポイントになった後に、Wifiアクセスポイントにつながる方法があるようです。
今回紹介するのは、いったん自身がBLEペリフェラルになった後に、BLEでSSIDやパスワードを受け取ってWifiアクセスポイントにつながる方法です。
この方法であれば、わざわざWifiのSSIDやパスワードを入力するために、入力端末側のWifi接続先を変える必要がなくなります。
また、最近は、ブラウザにWeb Bluetooth APIというのがあり、ブラウザからSSID・パスワードを入力することができるようになります。

Arduino側

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <WiFi.h>

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID_1 "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define CHARACTERISTIC_UUID_2 "beb5483f-36e1-4688-b7f5-ea07361b26a8"
#define BLE_DEVICE_NAME  "MyESP32"

#define WIFI_MAX_COUNTER  30

std::string wifi_ssid;
std::string wifi_password;
std::string wifi_mask("this is the wifi mask.");

bool wifi_entered;

class MyCallbacks1: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      wifi_ssid = pCharacteristic->getValue();
      Serial.print("SSID:");
      Serial.println(wifi_ssid.c_str());
    }
};

class MyCallbacks2: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      wifi_password = pCharacteristic->getValue();
//      Serial.print("Password:");
//      Serial.println(wifi_password.c_str());

      wifi_entered = true;
    }
};

IPAddress wifi_connect_with_ble(std::string ssid, std::string password){
  wifi_ssid = ssid;
  wifi_password = password;

  BLEDevice::init(BLE_DEVICE_NAME);
  BLEServer *pServer = BLEDevice::createServer();
  BLEService *pService = pServer->createService(SERVICE_UUID);
  BLECharacteristic *pCharacteristic1 = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID_1,
                                         BLECharacteristic::PROPERTY_READ |
                                         BLECharacteristic::PROPERTY_WRITE
                                       );
  BLECharacteristic *pCharacteristic2 = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID_2,
                                         BLECharacteristic::PROPERTY_READ |
                                         BLECharacteristic::PROPERTY_WRITE
                                       );
  pCharacteristic1->setCallbacks(new MyCallbacks1());
  pCharacteristic1->setValue("Input SSID");
  pCharacteristic2->setCallbacks(new MyCallbacks2());
  pCharacteristic2->setValue("Input Password");

  BLEAdvertising *pAdvertising = pServer->getAdvertising();

  int wifi_counter;
  do{
    wifi_counter = WIFI_MAX_COUNTER;
    wifi_entered = false;

    if(wifi_ssid == "" || wifi_password == "" ){
      Serial.println("");
      Serial.println("Waiting Wifi password with BLE...");
      pService->start();
      pAdvertising->start();
      while(!wifi_entered){
        delay(1000);
        Serial.print(".");
      }
      pAdvertising->stop();
      pService->stop();

      Serial.println("");
    }

    for( int i = 0 ; i < wifi_password.length() && i < wifi_mask.length() ; i++ )
      wifi_password[i] ^= wifi_mask[i];

    Serial.println("Connecting to Wifi AP...");
    WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());
    while (WiFi.status() != WL_CONNECTED) {
      delay(1000);
      Serial.print(".");

      wifi_counter--;
      if( wifi_counter <= 0 )
        break;
    }
    wifi_password = "";
  }while( wifi_counter <= 0 );

  Serial.println("");

  return WiFi.localIP();
}

wifi_maskには秘匿の適当なマスク値を指定してください。
BLE通信路上を流れるWiFiパスワードを盗聴されにくくするためです。
★ですが、これでもやっぱり破られるので、ご利用は自己責任でお願いします。★

また、各UUIDも、各自適当な値を生成し設定してください。

入力する端末側は、BLEセントラルとして CHARACTERISTIC_UUID_1 のキャラクタリスティックックにSSIDをWriteし、CHARACTERISTIC_UUID_2 のキャラクタリスティックにパスワードをWriteすることで、ESP32は入力された値を使ってWiFi接続します。
ただし、 WIFI_MAX_COUNTER秒経過しても、WiFiアクセスポイントに接続できなければ、また、BLEペリフェラルとして、WiFiパスワードの入力待ちとなります。

あとは、setup()で、上記の関数wifi_connect_with_ble()を呼び出せばよいです。

void setup() {
  Serial.begin(9600);

  IPAddress ipaddress = wifi_connect_with_ble("", "");
  Serial.print("Connected : ");
  Serial.println(ipaddress);
}


void loop() {
  // put your main code here, to run repeatedly:
  Serial.println("loop() called");
  delay(2000);
}

設定端末側

こんな感じのWebページを作ってみました。
Web Bluetooth APIが動作するOSであればよいので、Windows、Mac、AndroidのChromeで動きます。

image.png

esp32_nameとwifi_maskは、Arduino側に埋め込んだ値と同じ値を入力してください。
そして、wifi_ssid とwifi_password には、これから接続したいWiFiアクセスポイントのSSIDとパスワードを入力します。

BLEペリフェラルとして待ち受けているはずですので、「esp32_scan」ボタンを押下するとESP32が発見されます。
「esp32_connect」ボタンを押下すると、BLEペリフェラルに接続します。
接続完了すると、「esp32_write」ボタンが有効化されますので、押下します。これで、WiFiアクセスポイントの接続が始まります。

設定端末側のHTMLとJavascriptコードです。

start.js
'use strict';

//var vConsole = new VConsole();

const UUID_ESP32_SERVICE = '4fafc201-1fb5-459e-8fcc-c5c9c331914b';
const UUID_ESP32_CHARACTERISTIC_1 = 'beb5483e-36e1-4688-b7f5-ea07361b26a8';
const UUID_ESP32_CHARACTERISTIC_2 = 'beb5483f-36e1-4688-b7f5-ea07361b26a8';

var esp32_device = null;
var esp32_chars = new Map();

const encoder = new TextEncoder();

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        esp32_name: "MyESP32",
        wifi_ssid: '',
        wifi_password: '',
        wifi_mask: 'this is the wifi mask.',
        esp32_scaned: false,
        esp32_connected: false,
    },
    computed: {
    },
    methods: {
        onDisconnect: function(event){
            console.log('onDisconnect');
            this.esp32_connected = false;
        },

        esp32_scan: function(){
            return navigator.bluetooth.requestDevice({
                filters: [{ name: this.esp32_name}],
                optionalServices : [UUID_ESP32_SERVICE]
            })
            .then(device => {
                console.log("requestDevice OK");
                console.log(device);

                esp32_device = device;
                esp32_device.addEventListener('gattserverdisconnected', this.onDisconnect);
                this.esp32_scaned = true;
            })
            .catch(error =>{
                console.log(error);
            });
        },
        esp32_connect: function(){
            if( esp32_device.connected )
                return;

            return esp32_device.gatt.connect()
            .then(server => {
                console.log('Execute : getPrimaryService');
                return server.getPrimaryService(UUID_ESP32_SERVICE);
            })
            .then(service => {
                console.log('Execute : getCharacteristic');
                return Promise.all([
                    set_characteristic(service, UUID_ESP32_CHARACTERISTIC_1),
                    set_characteristic(service, UUID_ESP32_CHARACTERISTIC_2),
                ]);
            })
            .then(()=>{
                this.esp32_connected = true;
                console.log('Connected!!');
            })
            .catch(error =>{
                console.log(error);
            });
        },
        esp32_write: async function(){
            await esp32_chars.get(UUID_ESP32_CHARACTERISTIC_1).writeValue(encoder.encode(this.wifi_ssid));

            var buffer = encoder.encode(this.wifi_password);
            var mask = encoder.encode(this.wifi_mask);
            for( var i = 0 ; i < buffer.length && i < mask.length ; i++ )
                buffer[i] ^= mask[i];
            await esp32_chars.get(UUID_ESP32_CHARACTERISTIC_2).writeValue(buffer);

            alert('wrote!');
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function set_characteristic(service, characteristicUuid) {
    return service.getCharacteristic(characteristicUuid)
    .then(characteristic => {
        console.log('setCharacteristic : ' + characteristicUuid);
        esp32_chars.set(characteristicUuid, characteristic);
        return service;
    });
}
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>WiFi SSID/パスワード設定</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>WiFi SSID/パスワード設定</h1>

        <label>esp32_name</label> <input type="text" class="form-control" v-model="esp32_name">
        <button class="btn btn-default" v-on:click="esp32_scan">esp32_scan</button>
        <label>esp32_scaned</label> {{esp32_scaned}}
        <br>
        <br>
        <button class="btn btn-default" v-on:click="esp32_connect">esp32_connect</button>
        <label>esp32_connected</label> {{esp32_connected}}
        <br>
        <br>
        <label>wifi_ssid</label> <input type="text" class="form-control" v-model="wifi_ssid">
        <label>wifi_password</label> <input type="text" class="form-control" v-model="wifi_password">
        <label>wifi_mask</label> <input type="text" class="form-control" v-model="wifi_mask"><br>
        <button v-bind:disabled="!esp32_connected" class="btn btn-default" v-on:click="esp32_write">esp32_write</button>

        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>

そうそう、HTML5のWeb Bluetooth APIを使うので、上記Webページは、HTTPSにホスティングしてください。

補足

ほんとは、chirpを使いたかったんだけど、SDKをダウンロードできない。。。

以上

poruruba
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした