気の向くままに辿るIT/ICT/IoT
webzoit.net
IoT・電子工作

WiFi/サーボでペンダントライトをON/OFF 自作スマートプルスイッチ

ウェブ造ホーム前へ次へ
サイト内検索
カスタム検索
ESP8266って?

WiFi/サーボでペンダントライトをON/OFF 自作スマートプルスイッチ

WiFi/サーボでペンダントライトをON/OFF 自作スマートプルスイッチ

2020/03/07

 Wi-Fi(wifi)モジュールESP8266の姉妹チップESP32の内、38ピンある開発ボード1つとサーボモータ1つを使ってWiFi越しにペンダントライト(ペンダントランプ・傘&紐付き蛍光灯)をON/OFF(点灯/消灯・点けたり/消したり)するガジェット(スマートスイッチというか、スマートプルスイッチ)を作ってみるページ。

 改めて探してもたどり着かなかったものの、IoTを始めたての頃だったか、Arduinoによるものだったと思いますが、その他の具体的な方法論はともかく、傘付き&紐付きの蛍光灯の紐or根元をサーボモータで引っ張っているページを拝見して以来、頭の片隅にあったものを具現化してみた話です。

 これ、スマートライトと呼んでもよいんですかね?

ペンダントライト無線ON/OFFサーボ試作

 できれば手持ちの、DCモータ、ギヤモータ、ステッピングモータ、サーボモータを使って何か実用品を作りたい!シリーズ、Arduinoとステッピングモータでロールカーテン、これを無線操作できるようにしたESP8266・ESP32/MQTTでロールカーテン/ESP8266・ESP32/WebSocketでロールカーテン、物理スイッチ操作となるESP8266・ESP32/WebSocket/サーボで壁面照明スイッチON/OFFWebカメラのパン/チルトに続く、今回のペンダントライト無線遠隔ON/OFFサーボ。

 つい先日、手持ちのモータではトルク不足で棚上げしたものの、普通のカーテンの電動化もやってみました...[2020/04/4]と思ったらできましたArduino/ステップモータ28BYJ-48-5V/ラダーチェーンで既存のカーテンを電動化

 WebSocketサーバは、ESP32上に、WebSocketクライアント(ws://、SSL対応はwss://で始まるドメインやアドレスへのアクセス)には、JavaScriptやPythonを使いました。

ペンダントライト無線ON/OFFサーボ駆動部試作

 今回、無線・電動化はしますが、これと併せて、これまでのようにペンダントランプに付属の紐を引っ張ってON/OFFすることもできるようにします。

 そのため、紐付きの引掛けシーリングの紐というか、根元の金具(プルスイッチ)とサーボに針金をかけ、サーボでは、ここを引っ張ってON/OFFします。

 サーボホーンのスイングするスペースを確保するため、消しゴムや板ゴムでとりあえず嵩増ししています。

 一番は、サーボをガッツリ固定することですが、サーボの向き、嵩増し具合、ホーンと引掛けシーリングの距離などによってスイングさせる角度、ホーンのどの穴を針金でつなぐか、針金をどの程度締めるか(余らせるか)も変わってくるので微調整も重要です。

 回路は空中配線かユニバーサルボードを使ってこじんまりさせるとしてサーボの良い固定方法があれば、完成なんですが、思いつかない...。

ペンダントライト無線ON/OFFサーボ駆動部スイッチ付き電源タップ

 更に天井付近・傘の上で完結するようにコンセント付き引掛けシーリングを使い、ESP32用5Vとサーボモータ用5Vのように電源を2系統とっています。

 また、スイッチ付き電源タップを使い、電動部の電源を落とせるようにしました。

 と言ってもサーボ仮固定に養生テープを巻いたため、スイッチ付き電源タップのスイッチも全く押せないわけではないものの、スイッチの意味がない...。

 引掛けシーリングの順を変えると機能しなくなるため、コンセント付き引掛けシーリングを最初に付ける必要があり、そうすると集中スイッチ付き2口コンセントタップが挿せません。

 そこでスイッチ付き1口タップに3口は要らないものの、手元にあった3口電源タップ、そこにモーター用ACアダプタ5V/2AとUSB充電ACアダプタを接続しています。

 が、意図せず、スイッチが押せなくなってしまった問題を一時回避するため、集中スイッチ付き2口電源タップを追加でかませることにしました。

 つまり、引掛シーリングコンセント-スイッチ付き1口電源タップ-3口電源タップ-集中スイッチ付き2口電源タップ、ここから2系統電源をとると同時に、2口電源タップ側のスイッチをON/OFFすることにしました。(外すのが面倒なのでなんですが、こうなるとそもそもスイッチ付き1口電源タップは不要...。)

 尚、今回、サーボと同じくらいメインパーツの1つとも言えるこの引掛けシーリング、結果、3段重ねというメーカーが推奨していない(しないでねという注意書きのある)使い方をするので、もし、やってみるなら、自己責任で。

ペンダントライト無線ON/OFFサーボ駆動部スイッチ付き電源タップを変更
2020/03/11

 意味がなくなっていたスイッチ付き1口電源タップを外すことにしました。

 が、これに挿していた横長で左・右・下に3口あるテーブルタップを直接、引掛けシーリングのコンセントに挿すと集中スイッチ付き2口電源タップの取付ができなくなることが判明。

 そこで手持ちの縦長で下とサイドの一方に2口ある3口テーブルタップと交換することにしました。

 この時、同色系の白いテープで比較的がっつり、きれいに巻いたので仮配線部はともかく、これを以てサーボ取付部は完成としても良さそうです。

 この特殊な配置の電源タップ、どこで買ったかは覚えていませんが、デスク上の個別スイッチ付き6口電源タップの1つに挿していて1口を未使用のまま、Raspberry Piサーバ及びNAS用2TB HDDのACアダプタに使用していたもの。

 ちなみに、この作業にあたり、偶然にも1つで事足りる、ここ用に切り出したのかというくらいにピッタリの程よい木片があったため、消しゴム・板ゴムと交換しました。

 が、脚立には乗っていたものの、天井スレスレな位置ゆえに、やりづらい姿勢だったこともあり、何かの拍子に一段めの引掛けシーリングをひねってしまい、手をすり抜けて落下、がっしゃーん...。

 幸い蛍光灯は無事だったものの、傘が結構大きく割れて2つに分離してしまい、グルーガンで補修...っていうか見た目はともかく、グルーガン凄し...。

乳白色のボトルを使ったペンダントライト無線ON/OFFサーボ用マイコン・配線収納
2020/03/14

 当初思い描いていた通り、手持ちの中から乳白色のボトルを少し加工しつつ、照明のケーブルに抱かせる恰好でESPボードやケーブル類を収納してみました。

 自画自賛ながら、色合いも相まって照明器具の一部のようにも見えますし、天井も白く、あまり目立ちません。

 あえてカットした位置が見えるようにしましたが、ボトルの首を落とし、ボトル接合部らしきところをサイドから下部中央あたりまでノコで縦にカット。

 ボトル下部の中心あたりに加工用にしているハンダゴテで丸穴を開け、口の開いたボトルを逆さにした状態でESPボードや配線をボトルの中に詰め込んで配線は照明ケーブルと抱かせてその穴から出す感じに。

 無色透明のボトルにしようかとも思いましたが、天井や照明の傘と同色系だし、透明だと中がぐちゃぐちゃなのも見えちゃうし...ということで乳白色。

 最終的にパカパカしないようにタイトにカットした分、収納はしづらくなってしまいました。

 今のところ、ブレッドボードにジャンパワイヤを挿したりしたままなので尚更...。

 それも含め、ESPモジュールはWiFiのおかげで相応の電流が必要で相応の熱を持つこともあり、限りになく密閉に近い状態、真夏...などの条件を考慮すると、通気性の確保や中の収まりをちゃんとしておかないと発火、断線、照明落下、負傷、火事、近所や関係各位にご迷惑をおかけすることに...なんてことになるかもしれないので要注意...。

 そういう意味では、透明ボトルで見える化した方が安全確認もできますし、見方によっては、ESPボードの電源LEDが乳白色越しよりも無色透明の方が綺麗かも?

 自身は、面倒を押して運用上、基本、全てのIoTガジェットは、外からの操作は行わず、在宅時以外、このペンダントライトに至っては、就寝中も電源を落とすようにして(そうするために全て電源は、スイッチ付きコンセントからとって)います。

 そのため、異変があれば、基本、すぐに気づくはずなので大事に至ることはないと思いますが、消し忘れなどを考慮すると、やはり、安全の上にも安全を期したいところ。

2020/03/24

 数本、手持ちがありながら、なぜ、最初からそうしなかったのか我ながら謎もUSBケーブルを黒から白のフラットケーブルに変更しました。

 ところで今回の作ってみるシリーズ?、以下のように方法については、いろいろ考えてみたのですが、当初思い描いていたサーボに落ち着きました。

市販の引掛けシーリング対応後付赤外線リモコン
昨今、モノや評価の高低に関わらず、レビューを眺めていると萎える情報が多すぎる
自作魂の熱の逃し方に困る
WiFiリモコン化の方が操作範囲が広い
市販リモコン&WiFi化何れも実施するとコストアップ
...etc.
市販のWiFiスマートプラグ
スマートプラグに挿すために既存ペンダントランプの引掛シーリングを外してコンセントプラグに交換、ESPボードとサーボ用コンセント付き引掛けシーリングの他にもう1つコンセント付き、もしくは、コンセントアダプタタイプの引掛シーリングが必要...なまでは、まだよいとしても何れかのコンセントタイプの引掛シーリング-WiFiスマートコンセント-ペンダントランプでは、耐荷重的に明らかに無理、天井に引っ掛けフックをねじ込むなどしてほぼ全ての荷重をこれにかける必要があるも天井に穴をあけたくないし、物理的にも難しそうだし、代替策も思いつかない
自発的にならまだしも万が一にも位置情報を取得されたり、意図せず、外部とつながるのは気色悪くて嫌だから、やるにしても市販品ではなく、スマートプラグ自体自作を選択すると思うが、先の通り、ことペンダントランプに関しては、物理的障害がない前提で手間をかけない限り、現実的ではなさ気
...etc.
リレーを使う
自作するにあたり、その容易さから最初に考えたものの、既存のペンダントランプ用引掛けシーリングは、ランプの滑りにくい被覆付きコードの通し穴付きであり、耐久性を維持しつつ、リレーを噛ませる方法を思いつかなかった
以前拝見したサーボによる記事のアナログさが、なんだかたまらず、記憶に残る
モータを使いたい衝動が収まらないし、手持ちの在庫を使いたい
...etc.

前置き

 最近、その便利さにすっかりハマったAruduinoOTAを使ってOTA(Over The Air/無線)アップデートできるようにしました。

 3通りほどあるらしき、実装方法の内、Arduino IDEを使う前提のmDNS機能を必要とするものを選びました。

使ったもの

 ペンダントライトは除くとしてAmazon(Prime対応品)だと3500円前後、ESP32開発ボード、ACアダプタの価格差が特に大きくなるかとは思いますが、Aliexpressだと、2000円前後かと。

 材料については、個々の環境に合わせて適宜用意。

 ちなみに自身は、microUSBケーブル、精密ドライバー、針金、養生テープ、消しゴム、板ゴムなどは基本、100均のものを使っています。

前提

 mDNS機能を持つパッケージアプリケーションとしてLinuxならAvahi、Mac/WindowsならBonjourがインストール済みであること(macOSはBonjourはプリインストール済みのはず)。

 Arduino IDEが利用できることは、もちろん、ESP8266やESP32をArduino IDEで使えるようにしておくこと。

 ESP-01やESP-02〜ESP14などのESP8266チップなら、Arduino IDEの[ツール] => [ボード]から[Generic ESP8266 Module]を選択、ESPモジュールにスケッチをアップロードできる状態であること。

 ESP32なら、[espressif/arduino-esp32]の要領でESPモジュールにスケッチをアップロードできる状態であること。

 ちなみにこれらArduino IDEの環境設定で追加する方法の場合、カンマ区切りで複数指定可能。

回路

ESP8266 NodeMCUサーボ別電源備考
GPIO13信号線()-サーボ信号用
-プラス()5V-
GNDマイナス()マイナス

 5VのACアダプタを電源としたためか、12V/2A ACアダプタを供給源にステッピングモータでは電源を併用できた一方、サーボではできなかったため、今回は、ESP8266 NodeMCUの電源については、USB接続を前提にしています。

 が、別電源の条件によっては、ESP32もVINからの供給も可能と思われます。

 ちなみに今回の電源は、5V/2A ACアダプタでDCプラグが5.5x2.1mmではなかったため、切断し、端子台出しのプラグとジャックを接続、そこから(5Vなので)そのままジャンパワイヤでブレッドボードに供給しました。

AruduinoOTAアップデートがうまくいかない場合

 もし、OTA(On The Air/無線)アップデートがうまくいかない場合、サンプルスケッチBasicOTAにおいてWiFi SSIDとパスフレーズのみ環境に合わせ、NodeMCUボードにアップロードしてから、目的のスケッチをアップロードしてみるとよいかもしれません。

スケッチ

 今回のWebsocketサーバとなるESPボード側のスケッチは、こんな感じ。

#include <WiFi.h>
#include <WiFiClient.h>
#include <ArduinoOTA.h>
#include <ESP32WebServer.h>
#include <ESPmDNS.h>
#include <SPIFFS.h>
#include <FS.h>
#include <WebSocketsServer.h>
#include <Servo.h>
 
ESP32WebServer server(80);    // create a web server on port 80
WebSocketsServer webSocket(81);  // create a websocket server on port 81
 
File fsUploadFile;                  // a File variable to temporarily store the received file
 
const char* path_root  = "/index.html";
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
 
const char *ssid = "SSID"; // Wifi_STA network
const char *password = "PASSPHRASE"; // Wifi_STA passphrase
 
const char common_name[40] = "esp32pendantlight";
const char *OTAName = common_name;      // A name and a password for the OTA service
const char *mdnsName = common_name; // Domain name for the mDNS responder
 
#define INTERVAL_VS_BASE 1000
 
const int pull = 13;
 
Servo pullServo;
 
const int base = 0;
const int angle = 150;
/*__________________________________________________________SETUP__________________________________________________________*/
 
void setup() {
 delay(1000);
 Serial.begin(115200);
 delay(10);
 Serial.println("\r\n");
 
 pinMode(pull, OUTPUT);
 pullServo.attach(pull);
 
 startWiFi();
 startOTA();
 startSPIFFS();
 startWebSocket();
 startMDNS();
 startServer();
}
 
/*__________________________________________________________LOOP__________________________________________________________*/
 
void loop() {
 webSocket.loop();              // constantly check for websocket events
 server.handleClient();           // run the server
 ArduinoOTA.handle();            // listen for OTA events
 // ESP8266用Modem-sleepモード設定(自動復帰)
 wifi_set_sleep_type(LIGHT_SLEEP_T);
}
 
/*__________________________________________________________SETUP_FUNCTIONS__________________________________________________________*/
 
void startWiFi() { // Start a Wi-Fi access point, and try to connect to some given access points. Then wait for either an AP or STA connection
 // WiFi.softAP(ssid, password);       // Start the access point
 WiFi.mode(WIFI_STA);       // Start the access point
 WiFi.begin(ssid, password);       // Start the access point
 while ( WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
 }
 Serial.println("");
 Serial.print("SSID \"");
 Serial.print(ssid);
 Serial.println("\" started\r\n");
 
 Serial.print("Connected to ");
 Serial.println(ssid);
 
 Serial.print("IP address: ");
 Serial.println(WiFi.localIP());
 
 Serial.print("hostname : ");
 // Serial.println(WiFi.hostname());
 Serial.println("");
}
 
void startOTA() { // Start the OTA service
 ArduinoOTA.setHostname(OTAName);
 // ArduinoOTA.setPassword(OTAPassword);
 
 ArduinoOTA.onStart([]() {
  Serial.println("Start");
  // turn off the LEDs
  for (int i = 0; i < 6; i++) {
   //   digitalWrite(LED_BUILTIN, HIGH);
   //   digitalWrite(LED_BUILTIN, LOW);
  }
 });
 ArduinoOTA.onEnd([]() {
  Serial.println("\r\nEnd");
 });
 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
  Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
 });
 ArduinoOTA.onError([](ota_error_t error) {
  Serial.printf("Error[%u]: ", error);
  if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
  else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
  else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
  else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
  else if (error == OTA_END_ERROR) Serial.println("End Failed");
 });
 ArduinoOTA.begin();
 Serial.println("OTA ready\r\n");
}
 
//
// https://github.com/zhouhan0126/WebServer-esp32/blob/master/examples/FSBrowser/FSBrowser.ino
//
void listDir(fs::FS &fs, const char * dirname, uint8_t levels) {
 // Serial.printf("Listing directory: %s\n", dirname);
 
 File root = fs.open(dirname);
 if (!root) {
  Serial.println("Failed to open directory");
  return;
 }
 if (!root.isDirectory()) {
  Serial.println("Not a directory");
  return;
 }
 
 File file = root.openNextFile();
 while (file) {
  if (file.isDirectory()) {
   Serial.print(" DIR : ");
   Serial.println(file.name());
   if (levels) {
    listDir(fs, file.name(), levels - 1);
   }
  } else {
   Serial.print(" FILE: ");
   Serial.print(file.name());
   Serial.print(" SIZE: ");
   Serial.println(file.size());
  }
  file = root.openNextFile();
 }
}
 
void startSPIFFS() { // Start the SPIFFS and list all contents
 SPIFFS.begin();               // Start the SPI Flash File System (SPIFFS)
 Serial.println("SPIFFS started. Contents:");
 {
  listDir(SPIFFS, "/", 0);
 }
 Serial.printf("\n");
 /*
  Serial.println("SPIFFS started. Contents:");
  {
   Dir dir = SPIFFS.openDir("/");
   while (dir.next()) {           // List the file system contents
    String fileName = dir.fileName();
    size_t fileSize = dir.fileSize();
    Serial.printf("\tFS File: %s, size: %s\r\n", fileName.c_str(), formatBytes(fileSize).c_str());
   }
   Serial.printf("\n");
  }
 */
}
 
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght) { // When a WebSocket message is received
 switch (type) {
  case WStype_DISCONNECTED:       // if the websocket is disconnected
   Serial.printf("[%u] Disconnected!\n", num);
   break;
  case WStype_CONNECTED: {       // if a new websocket connection is established
    IPAddress ip = webSocket.remoteIP(num);
    Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
   }
   break;
  case WStype_TEXT:           // if new text data is received
   Serial.printf("[%u] get Text: %s\n", num, payload);
   Serial.print("payload[0] : ");
   Serial.println(payload[0]);
 
   delay(INTERVAL_VS_BASE);
   if (payload[0] == '1') {
     pullServo.write(angle);
     adjust_base();
   }
   break;
 }
}
 
void handleFileUpload() { // upload a new file to the SPIFFS
 HTTPUpload& upload = server.upload();
 String path;
 if (upload.status == UPLOAD_FILE_START) {
  path = upload.filename;
  if (!path.startsWith("/")) path = "/" + path;
  if (!path.endsWith(".gz")) {             // The file server always prefers a compressed version of a file
   String pathWithGz = path + ".gz";         // So if an uploaded file is not compressed, the existing compressed
   if (SPIFFS.exists(pathWithGz))           // version of that file must be deleted (if it exists)
    SPIFFS.remove(pathWithGz);
  }
  Serial.print("handleFileUpload Name: "); Serial.println(path);
  fsUploadFile = SPIFFS.open(path, "w");      // Open the file for writing in SPIFFS (create if it doesn't exist)
  path = String();
 } else if (upload.status == UPLOAD_FILE_WRITE) {
  if (fsUploadFile)
   fsUploadFile.write(upload.buf, upload.currentSize); // Write the received bytes to the file
 } else if (upload.status == UPLOAD_FILE_END) {
  if (fsUploadFile) {                  // If the file was successfully created
   fsUploadFile.close();                // Close the file again
   Serial.print("handleFileUpload Size: "); Serial.println(upload.totalSize);
   server.sendHeader("Location", "/success.html");   // Redirect the client to the success page
   server.send(303);
  } else {
   server.send(500, "text/plain", "500: couldn't create file");
  }
 }
}
 
void startWebSocket() { // Start a WebSocket server
 webSocket.begin();             // start the websocket server
 webSocket.onEvent(webSocketEvent);     // if there's an incomming websocket message, go to function 'webSocketEvent'
 Serial.println("WebSocket server started.");
}
 
void startMDNS() { // Start the mDNS responder
 MDNS.begin(mdnsName);            // start the multicast domain name server
 Serial.print("mDNS responder started: http://");
 Serial.print(mdnsName);
 Serial.println(".local");
}
 
void startServer() { // Start a HTTP server with a file read handler and an upload handler
 server.on("/", handleRoot);
 server.on("/edit.html", HTTP_POST, []() { // If a POST request is sent to the /edit.html address,
  server.send(200, "text/plain", "");
 }, handleFileUpload);            // go to 'handleFileUpload'
 server.onNotFound(handleNotFound);     // if someone requests any other file or page, go to function 'handleNotFound'
 // and check if the file exists
 
 server.begin();               // start the HTTP server
 Serial.println("HTTP server started.");
}
 
/*__________________________________________________________Motor_FUNCTIONS__________________________________________________________*/
 
void adjust_base() {
 delay(INTERVAL_VS_BASE);
 pullServo.write(base);
}
 
/*__________________________________________________________HELPER_FUNCTIONS__________________________________________________________*/
 
String formatBytes(size_t bytes) { // convert sizes in bytes to KB and MB
 if (bytes < 1024) {
  return String(bytes) + "B";
 } else if (bytes < (1024 * 1024)) {
  return String(bytes / 1024.0) + "KB";
 } else if (bytes < (1024 * 1024 * 1024)) {
  return String(bytes / 1024.0 / 1024.0) + "MB";
 }
}
 
String getContentType(String filename) { // determine the filetype of a given filename, based on the extension
 if (filename.endsWith(".html")) return "text/html";
 else if (filename.endsWith(".css")) return "text/css";
 else if (filename.endsWith(".json")) return "text/css";
 else if (filename.endsWith(".js")) return "application/javascript";
 else if (filename.endsWith(".ico")) return "image/x-icon";
 else if (filename.endsWith(".png")) return "image/x-icon";
 else if (filename.endsWith(".gz")) return "application/x-gzip";
 return "text/plain";
}
/*__________________________________________________________SERVER_HANDLERS__________________________________________________________*/
 
void handleNotFound() { // if the requested file or page doesn't exist, return a 404 not found error
 if (!handleFileRead(server.uri())) {    // check if the file exists in the flash memory (SPIFFS), if so, send it
  server.send(404, "text/plain", "404: File Not Found");
 }
}
 
boolean readHTML() {
 File htmlFile = SPIFFS.open(path_root, "r");
 if (!htmlFile) {
  Serial.println("Failed to open index.html");
  return false;
 }
 size_t size = htmlFile.size();
 if (size >= BUFFER_SIZE) {
  Serial.print("File Size Error:");
  Serial.println((int)size);
 } else {
  Serial.print("File Size OK:");
  Serial.println((int)size);
 }
 // htmlFile.read(buf, size);
 htmlFile.close();
 return true;
}
 
bool handleFileRead(String path) { // send the right file to the client (if it exists)
 Serial.println("handleFileRead: " + path);
 if (path.endsWith("/")) path += "index.html";     // If a folder is requested, send the index file
 String contentType = getContentType(path);       // Get the MIME type
 String pathWithGz = path + ".gz";
 if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { // If the file exists, either as a compressed archive, or normal
  if (SPIFFS.exists(pathWithGz))             // If there's a compressed version available
   path += ".gz";                     // Use the compressed verion
  File file = SPIFFS.open(path, "r");          // Open the file
  size_t sent = server.streamFile(file, contentType);  // Send it to the client
  file.close();                     // Close the file again
  Serial.println(String("\tSent file: ") + path);
  return true;
 }
 Serial.println(String("\tFile Not Found: ") + path);  // If the file doesn't exist, return false
 return false;
}
 
void handleRoot() {
 Serial.println("Access");
 char message[20];
 String(server.arg(0)).toCharArray(message, 20);
 server.send(200, "text/html", (char *)buf);
}

 ESP32ボードを使うにあたり、以前、そんなことした記憶はないのですが、ESP32WebServer.hをincludeすべく、 Pedroalbuquerque / ESP32WebServerをArduino libraliesフォルダにダウンロードさせていただきました。

 また、コード内に参照URLがありますが、ESP32においては、startSPIFFS()内のコメントアウト部は、listDirlistDir(fs::FS &fs, const char * dirname, uint8_t levels){}関数を追記の上、listDir(SPIFFS, "/", 0);としないと機能しないようです。

 adjust_base()関数を作ってあるものの、やることが少なすぎてあえて作るまでもないでしょう。

 便利なもので後述のように(今回は、JavaScriptから)WebSocketのパスが呼ばれるとESP8266/ESP32にアップロードしたスケッチのwebSocketEvent関数がコールされ、payload引数にその値が入ってきます。

 WebScokets.hのライブラリや、ベースとさせて頂いたスケッチにおける基本的な修正点などについては、ESP8266・ESP32/WebSocket自作無線電動ロールスクリーン同様です。

 ESP8266 NodeMCUを使う場合には、include行においてSPIFFS.h、WiFiClient.hは不要、WiFi.h、ESP32WebServer.h、ESPmDNS.hは、それぞれ、ESP8266WiFi.h、ESP8266WebServer.h、ESP8266mDNS.hに、これに伴い、ESP32WebServer server(80);行は、ESP8266WebServer server(80);に変更、startSPIFFS()内のlistDir(SPIFFS, "/", 0);は、コメントアウト部と入れ替えます。

HTML/JavaScript

<!DOCTYPE html>
<html>
<head>
<title>ルームライト</title>
<link href='main.css' rel='stylesheet' type='text/css' />
<meta charset="utf-8" />
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport' />
<script src='WebSocket.js' type='text/javascript'></script>
<script>
//var rainbowEnable = false;
var connection = new WebSocket('ws://'+location.hostname+':81/', ['arduino']);
connection.onopen = function () {
 connection.send('Connect ' + new Date());
};
connection.onerror = function (error) {
 console.log('WebSocket Error ', error);
};
connection.onmessage = function (e) {
 console.log('Server: ', e.data);
};
connection.onclose = function(){
 console.log('WebSocket connection closed');
};
 
function sendCtrl(btn) {
 document.getElementById("data").value = btn
 console.log('Btn Data: ' + btn);
 connection.send(btn);
}
function http_req(url) {
 var req = new XMLHttpRequest();
 req.open("GET", url, true);
 req.send();
}
 
function sw() {
 document.getElementById("data").value = "0";
 if (document.getElementById("onoff").src.match("switch_right.png")) {
  document.getElementById("onoff").src="switch_left.png";
 } else if (document.getElementById("onoff").src.match("switch_left.png")) {
  document.getElementById("onoff").src="switch_right.png";
 }
 sendCtrl('1');
}
</script>
</head>
 
<body>
<center>
<header>
<h1>ルームライト</h1>
</header>
<div class="ctrlbtn"><input id="onoff" type="image" src="switch_left.png" onclick="sw();" value="ペンダントライトON" style="width:160px ;height:160px ;"></div>
<div style="clear:both ;"></div>
<div><input id="data" type="text" value="0"></div>
<div><input type="button" name="mainmenu" value="to Main Menu" onClick="http_req(location.href='http://esphamainsrv.local')"></div>
</center>
</body>
</html>

 テキストボックス(input type=text)は、クリック時の送信データ確認用、今回は1しかないので明らかに運用時は不要です。

 以前作ったsendCtrl()を再利用しただけで、sw()と統合するというか、sendCtrl()を修正するほうがスマートでしょう。

操作方法

ESP32+サーボ+引掛けシーリングペンダントライト用ブラウザ版操作パネルのボタン例1

 ブラウザにmDNS名でxxx.local/index.htmlか、IPアドレスでxxx.xxx.xxx.xxx/index.htmlにアクセスするとスイッチ1つとテキストボックス、スマートホームコントロールパネルへのボタンのあるペンダントライト操作パネルが表示されます。

 スイッチは、左右に押下するタイプで、これをクリックすると左か右に押された画像に変わると同時にESP32によるWebSocketサーバにWebSocketクライアントであるJavaScriptからデータ(今回は、何れにしても'1'のみ)が送信されます。

ESP32+サーボ+引掛けシーリングペンダントライト用ブラウザ版操作パネルのボタン例2

 JavaScriptでブラウザからArduinoのLEDをON/OFFに倣って周囲が暗くなると同時にランプ点灯のような凝ったスイッチにしていたのですが、よく考えたら、紐付き引掛シーリングと紐付きペンダントランプの関係から状況によってはオンとオフを特定できないことに気づき、どっちがONでもOFFでも気にならない画像にすることにしました。

 これなら、別途、ESP8266/ESP32で作って運用中の集中操作パネルとしているブラウザ版スマートホーム操作パネルに簡単に統合できます。

スクリプトから操作

$ pip3 install websocket-client-py3
$ cat pendantlight.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
 
from websocket import create_connection
import sys
 
ws = create_connection("ws://esp32pendantlight:81/")
arg = sys.argv
 
ws.send(arg[1])
ws.close()
$ chmod +u pendantlight.py
$ ./pendantlight.py 1

 例えば、Python(Python3)だと、まず、WebSocketクライアント(今回は、pipでwebsocket-client-py3)をインストール、スクリプトをこんな風に書けば、引数を渡して操作できます。

 このようにスクリプトから操作できると自作ラズパイスマートスピーカーでダイニングキッチンライトスイッチ切り替えサーボを音声操作するのも容易にできるようになります。

2020/04/27

 Raspberry Pi/ESPボード/WebSocketクライアントの組み合わせについての注意・特記事項

自作スマートスピーカー用Qtデスクトップ操作パネル

 また、ブラウザ版操作パネルをデスクトップ版操作パネルから呼ぶこともできます。

ウェブ造ホーム前へ次へ