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

WiFi/サーボで壁の照明スイッチをON/OFF 自作スマートスイッチ

ホーム前へ次へ
ESP8266って?

WiFi/サーボで壁の照明スイッチをON/OFF 自作スマートスイッチ

WiFi/サーボで壁の照明スイッチをON/OFF 自作スマートスイッチ

2020/03/01

 Wi-Fi(wifi)モジュールESP8266の内、NodeMCU開発ボード1つとサーボモータ2つを使ってWiFi越しに縦並びの物理スイッチ(壁埋め込みの照明・電気スイッチ)2つをON/OFFするガジェットを作ってみるページ。

 スマートとは言い難い気もしますが、一応、スマートスイッチ。

 以前、やってみた電源タップでのArduino+サーボによるON/OFFデモを無線化し、実用的にしたもの。

壁面照明スイッチ切り替えサーボモータ

 取り付け方法にあぐねていたこともあり、必要性の点からも後回しにしてきました。

 が、DCモータ、ギヤモータ、ステッピングモータ、サーボモータを使って何か実用品を作りたい!と思いつつ、なかなか思いつかない時は、やっぱりこれだよね、ということで今やることにした次第。

 電動化後も、もちろん、手動での操作もできます。

 無線操作するにあたり、ソフト的には、ESP8266・ESP32/WebSocketによる自作無線電動ロールスクリーンのステッピングモータをサーボモータに代えたに過ぎません。

 結果、PC/スマホ/タブレットなどのブラウザからの操作だけでなく、スクリプト化し、デスクトップ版操作パネルからブラウザ操作パネルを表示したり、端末(≒ターミナル)からはもちろん、自作スマートスピーカーからの音声操作もできます。

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

前置き

 完成後は不要かもしれませんが、モノがモノ、場所が場所だけにAruduinoOTAを使ってOTA(Over The Air/無線)アップデートできるようにしました。

 そうそう要らないでしょ...と思っていたのですが、今回のケースでは、超絶便利でした。

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

取付場所・取付方法

壁面スイッチ切り替えサーボモータ構造体背面

 茶棚側面が10cmほどの距離で面した壁面スイッチにおいて茶棚側に画像の構造物を設置。

 茶棚天板にL字ステー、これにストレートステーをつなげ、立ちおろし、ここに画像の構造物をボルト締め。

壁面スイッチ切り替えサーボモータ構造体背面

 2つのL字ステー及び土台のストレートステーを共締めしたボルトの突起と、このL字ステーと先端のサーボ固定用小型L字ステーの大穴、長穴がスイッチまでの調整代(しろ)。

 と言いつつ、これらのボルトは調整しろにするには、わずかに短く足りませんでした。

 結局、微調整用のボルトナットとゴム板を駆使、その間、数回、茶棚自体も数mm?微妙に動かしたりもしました。

 下のスイッチは完璧も逆さに付けたからか、個体の問題か、上のサーボがたまにびびることがあり、スイッチをONにした瞬間にOFFにしてしまったりで、それまで驚くほど思い描いた通りで順調だった中、最終的なスイッチまでの位置決めに最も時間がかかりました。

 もう少し、スマートに位置決めできないものかと今尚、考えているほど。

[2020/12/20]
 下向きに付けたサーボは、この半年以内だと思いますが、回転角が小さくなったりして著しく成功率が下がってきたので1回交換、更に、ここのところ、ロックがかかったかのように指で回してさえ、サーボホーンが動かなくなることも重なり、プラスチック製ギアのSG90から金属ギアのMG90Sに交換、少なくとも下方向などイレギュラーな?向きに付けるサーボは、金属ギアなどより頑強なサーボの方が良いようです。

[2020/12/28]
 騙し騙し使ってきましたが、不安定なので正常な向きのサーボもMG90Sに替えました。
 やはり、SG90は実験程度に留めた方が良さそうです。

[2024/06/01]
 あれから数年、下段スイッチ側の上向きに設置したサーボは安定稼働している一方、逆さまにした上部スイッチの方は、金属サーボにしても動作が不安定となることもあり、ソフトウェアリセットや電源OFF/ONの強制リセットをしたりもして、それこそ騙し騙し使ってきたのですが、思いきってお気に入りのユニポーラのままのステッピングモータ28BYJ-48 5VとステップモータドライバULN2003に替えることにしました。
 28BYJと適度なサイズにカットしたMDF板をフランジ式のシャフトカプラーで固定。
 上にしても下にしても横にしても安定していて良い感じの28BYJ...って、どっちが上かわかりませんが、ステー構造物上部からシャフト・軸を下に向け、ステーをモータ取り付け穴と適度な長さに切ったタミヤのユニバーサルアームで鋏み、ボルトとナットで2箇所固定。
 スイッチとの間隔は、ステー上の長穴サイズ内の良き位置に微調整。
 変更したスケッチは、後述。

壁面スイッチ切り替えサーボモータ構造体

 設置する向きは、こんな感じです。

 尚、今回対象となる照明用スイッチは、大きめである上、ON/OFFで左右ともに押さなければいけないもので試す前は、ホーンに何か抱かせた方が...とも思ったものの、サーボSG90の付属ホーンでいけました。

制御部

自作スマートスイッチコントローラ
[2021/06/20]

 仮設のまま常用していましたが、重い腰を上げて、ガジェット筐体として、すっかり、お気に入りの100均セリアでトレカケース、キャンドゥで名刺ケースとして売っているケースに入れることにしました。

 配線用に切り欠きを入れて放り込んだだけですが...。

 また、下のアームにぶら下げるべく、なんかないかと思案、ちょうど買ってきたばかりのダイソーやキャンドゥで売っている大網の白い滑り止めマットに目が止まり、適当なサイズに切り、2つ折りにして繰り返し使える結束バンド2本を使うことにしました。

 スペース的にもほぼぴったりであることもあり、有事の際の取り出しやすさ、筐体が落ちない点を重視、2つ折りにしてあるだけなのでよじって横から取り出すことができ、滑り止めマットだけに滑り落ちることもなく、大判の網網なので視認性もよく、周囲の色と同色の白ということで完璧。

 これまで400穴のブレッドボードにブレッドボード用電源、ACアダプタ12Vから電源供給しつつ、ESPボードにはUSB給電、サーボ2つ駆動というなんともかさばる構成を見えないことを良いことに茶棚側面に養生テープやブル・タックで仮留めした状態でしたが、ついでに電源をACアダプタ12Vのみからとり、降圧モジュールを追加、ミニブレッドボードに替えたので一気にすっきりしました。

 ちなみにサーボの固定は養生テープから盛り盛りのブル・タックに替え、概ね良好も、この用途には、良くも悪くも粘度があって動作時の反動により、特に逆さまにしている上部スイッチ用は、極たまにスイッチ方向に押して近づけておいてあげる必要があります。

[後日談] 上のアームも巻き込んでブルタックをモリモリにしたら安定しました。

 あ、画質落としすぎて茶棚や壁は特に荒くなってますね...。

自作スマートスイッチコントローラ Fritzing

 一応、Fritzing。

使ったもの

 Amazon(Prime対応品)だと4000円前後、Aliexpressだと、これの4〜5割くらいで1500〜2000円あたりかと。

 ステーは、300mmのストレートステー1つは100均(ダイソー)、それ以外のステーは、ホームセンターで使った分は600円前後、ステー固定用ネジやステー間固定用ボルト・ワッシャー・ナットも100均でセットで300円前後、単品なら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サーボ別電源備考
D4/GPIO02信号線()-サーボ1用
D8/GPIO15信号線()-サーボ2用
-プラス()5V-
GNDマイナス()マイナス

 ブレッドボード用電源を介した場合、サーボを2つ使ったからか、ステッピングモータではできた一方、サーボ2つではできなかったため、今回は、ESP8266 NodeMCUの電源については、USB接続を前提にしています。

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

 また、よく調べていませんが、ESP8266 NodeMCUボードにおいては、2つのサーボを操作するにあたり、何通りか試したところ、できないポートもあるようでサーボの信号線につなぐポートは、D4/GPIO02とD8/GPIO15ならいけました。

 ブレッドボード電源に降圧を任せた今回、電源投入直後、たまに基準位置合わせに失敗することがあったり、停止時にビビリ音と共にサーボが振動(何れもわずか)することがありますが、適切な降圧コンバータを使うなど電源を強化すれば解消するのではと思っています。

 今回は、壁面スイッチの真下下方にコンセントがあるため、ここに集中スイッチ付き2口電源タップを、そこに別電源とした12V/2A ACアダプタとUSB充電ACアダプタを挿すことにしましたが、電源をVINからとれるなら、もちろん2口電源タップやUSB充電ACアダプタ、USBケーブルは不要です。

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

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

スケッチ

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

#include <ESP8266WiFi.h>
#include <ArduinoOTA.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <FS.h>
#include <WebSocketsServer.h>
#include <Servo.h>
 
ESP8266WebServer 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
 
#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] = "espdk_light";
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 500
 
const int dining_light_sw = D4;
const int kitchen_light_sw = D8;
 
Servo diningServo;
Servo kitchenServo;
 
const int forward = 135;  // 正転角度
const int base = 90;    // 基準角度
const int backward = 45;  // 逆転角度
 
/*__________________________________________________________SETUP__________________________________________________________*/
 
void setup() {
 delay(1000);
 Serial.begin(115200);
 delay(10);
 Serial.println("\r\n");
 
 diningServo.attach(dining_light_sw);
 kitchenServo.attach(kitchen_light_sw);
 
 diningServo.write(base);
 kitchenServo.write(base);
 
 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");
}
 
void startSPIFFS() { // Start the SPIFFS and list all contents
 SPIFFS.begin();               // Start the SPI Flash File System (SPIFFS)
 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 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.");
}
 
/*__________________________________________________________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");
 }
}
 
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);
}
 
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 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);
    //    rainbow = false;         // Turn rainbow off when a new connection is established
   }
   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);
   adjust_base();
   if (payload[0] == '1') {
    delay(INTERVAL_VS_BASE);
    diningServo.write(forward);
    delay(INTERVAL_VS_BASE);
    diningServo.write(base);
    delay(INTERVAL_VS_BASE);
   } else if (payload[0] == '2') {
    delay(INTERVAL_VS_BASE);
    diningServo.write(backward);
    delay(INTERVAL_VS_BASE);
    diningServo.write(base);
    delay(INTERVAL_VS_BASE);
   } else if (payload[0] == '3') {
    delay(INTERVAL_VS_BASE);
    kitchenServo.write(backward);
    delay(INTERVAL_VS_BASE);
    kitchenServo.write(base);
    delay(INTERVAL_VS_BASE);
   } else if (payload[0] == '4') {
    delay(INTERVAL_VS_BASE);
    kitchenServo.write(forward);
    delay(INTERVAL_VS_BASE);
    kitchenServo.write(base);
    delay(INTERVAL_VS_BASE);
   }
   break;
 }
}
 
/*__________________________________________________________Motor_FUNCTIONS__________________________________________________________*/
 
void adjust_base() {
 delay(INTERVAL_VS_BASE);
 diningServo.write(base);
 delay(INTERVAL_VS_BASE);
 kitchenServo.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";
}

 WebSocket communicationプロジェクトのスケッチをベースにさせて頂いたものです。

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

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

2024/06/01

 前述の通り、上部スイッチ側の逆さ状態のサーボSG90から替えてあったMG90Sをステッピングモータ28BBYJ-48に替えたのに伴うスケッチは次のようになりました。

#include <WiFi.h>
#include <ArduinoOTA.h>
//#include <ESP32WebServer.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <SPIFFS.h>
#include <Arduino.h>
#include <FS.h>
#include <WebSocketsServer.h>
#include <ESP32Servo.h>
#include <Stepper.h>
#include "time.h"
 
//ESP32WebServer server(80);
WebServer server(80);
WebSocketsServer webSocket(81);
 
File fsUploadFile;
 
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
const char *ssid     = "SSID";
const char *password = "PASSPHRASE";
 
IPAddress local_IP(192, 168, 1, 200);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
//IPAddress primaryDNS(8, 8, 8, 8); //optional
//IPAddress secondaryDNS(8, 8, 4, 4); //optional
 
const char common_name[40] = "esp_living_dining_light";
const char *OTAName = common_name;
const char *mdnsName = common_name;
 
#define INTERVAL_VS_BASE 500
 
const int dining_light_sw = 26;
 
Servo diningServo;
 
const int forward = 132;
const int base = 90;
const int backward = 48;
 
// for Kitchen light switch
const int motorPin1 = 13;   // Blue  - 28BYJ48 pin 1
const int motorPin2 = 12;   // Pink  - 28BYJ48 pin 2
const int motorPin3 = 14;   // Yellow - 28BYJ48 pin 3
const int motorPin4 = 27;   // Orange - 28BYJ48 pin 4
// Red  - 28BYJ48 pin 5 (VCC)
 
const int stepsPerRevolution = 360;
int motorSpeed = 1200; //速度。数値が小さいほど速くなる。800以下は脱調して動かない
// 要素lookup[9]は停止用
//int lookup[8] = {B01000, B01100, B00100, B00110, B00010, B00011, B00001, B01001};
int lookup[9] = {B01000, B01100, B00100, B00110, B00010, B00011, B00001, B01001, B00000};
 
const int turn_rev = 20;
const int return_rev = 20;
 
Stepper myStepper(stepsPerRevolution, motorPin1, motorPin2, motorPin3, motorPin4);
 
unsigned int stop_time = 1;
 
void setup() {
  delay(1000);
  Serial.begin(115200);
  delay(10);
  Serial.println("\r\n");
 
  pinMode(motorPin1, OUTPUT);
  pinMode(motorPin2, OUTPUT);
  pinMode(motorPin3, OUTPUT);
  pinMode(motorPin4, OUTPUT);
 
  diningServo.attach(dining_light_sw);
  diningServo.write(base);
 
  startWiFi();
  startOTA();
  startSPIFFS();
  startWebSocket();
  startMDNS();
  startServer();
}
 
unsigned long prevMillis = millis();
 
void loop() {
  webSocket.loop();
  server.handleClient();
  ArduinoOTA.handle();
}
 
void startWiFi() {
  WiFi.disconnect();
 
  if (!WiFi.config(local_IP, gateway, subnet)) {
    Serial.println("STA Failed to configure");
  }
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  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() {
  SPIFFS.begin();
  Serial.println("SPIFFS started. Contents:");
  {
    listDir(SPIFFS, "/", 0);
    /*
    Dir dir = SPIFFS.openDir("/");
    while (dir.next()) {
      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 startWebSocket() {
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
  Serial.println("WebSocket server started.");
}
 
void startMDNS() {
  MDNS.begin(mdnsName);
  Serial.print("mDNS responder started: http://");
  Serial.print(mdnsName);
  Serial.println(".local");
}
 
void startServer() {
  server.on("/", handleRoot);
  server.on("/edit.html",  HTTP_POST, []() {
    server.send(200, "text/plain", "");
  }, handleFileUpload);
 
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("HTTP server started.");
}
 
void handleNotFound() {
  if (!handleFileRead(server.uri())) {
    server.send(404, "text/plain", "404: File Not Found");
  }
}
 
bool handleFileRead(String path) {
  Serial.println("handleFileRead: " + path);
  if (path.endsWith("/")) path += "index.html";
  String contentType = getContentType(path);
  String pathWithGz = path + ".gz";
  if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) {
    if (SPIFFS.exists(pathWithGz))
      path += ".gz";
    File file = SPIFFS.open(path, "r");
    size_t sent = server.streamFile(file, contentType);
    file.close();
    Serial.println(String("\tSent file: ") + path);
    return true;
  }
  Serial.println(String("\tFile Not Found: ") + path);
  return false;
}
 
void handleRoot() {
  Serial.println("Access");
  char message[20];
  String(server.arg(0)).toCharArray(message, 20);
  server.send(200, "text/html", (char *)buf);
}
 
void handleFileUpload() {
  HTTPUpload& upload = server.upload();
  String path;
  if (upload.status == UPLOAD_FILE_START) {
    path = upload.filename;
    if (!path.startsWith("/")) path = "/" + path;
    if (!path.endsWith(".gz")) {
      String pathWithGz = path + ".gz";
      if (SPIFFS.exists(pathWithGz))
        SPIFFS.remove(pathWithGz);
    }
    Serial.print("handleFileUpload Name: "); Serial.println(path);
    fsUploadFile = SPIFFS.open(path, "w");
    path = String();
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    if (fsUploadFile)
      fsUploadFile.write(upload.buf, upload.currentSize);
  } else if (upload.status == UPLOAD_FILE_END) {
    if (fsUploadFile) {
      fsUploadFile.close();
      Serial.print("handleFileUpload Size: "); Serial.println(upload.totalSize);
      server.sendHeader("Location", "/success.html");
      server.send(303);
    } else {
      server.send(500, "text/plain", "500: couldn't create file");
    }
  }
}
 
void setOutput(int out)
{
  digitalWrite(motorPin1, bitRead(lookup[out], 0));
  digitalWrite(motorPin2, bitRead(lookup[out], 1));
  digitalWrite(motorPin3, bitRead(lookup[out], 2));
  digitalWrite(motorPin4, bitRead(lookup[out], 3));
}
 
void counterclockwise()  //反時計回り
{
  for (int i = 0; i < 8; i++)
  {
    setOutput(i);
    delayMicroseconds(motorSpeed);
  }
}
 
void clockwise()  //時計回り
{
  for (int i = 7; i >= 0; i--)
  {
    setOutput(i);
    delayMicroseconds(motorSpeed);
  }
}
 
void kitchen_light_on() {
  Serial.println("kitchen light on");
 
  for (int i = 0;i <= turn_rev;i++){
    counterclockwise();
  }
  for (int i = 0;i <= return_rev;i++){
    clockwise();
  }
  setOutput(9);
}
 
void kitchen_light_off() {
  Serial.println("kitchen light off");
 
  for (int i = 0;i <= turn_rev;i++){
    clockwise();
  }
  for (int i = 0;i <= return_rev;i++){
    counterclockwise();
  }
  setOutput(9);
}
 
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:
      Serial.printf("[%u] Disconnected!\n", num);
      break;
    case WStype_CONNECTED: {
        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:
      Serial.printf("[%u] get Text: %s\n", num, payload);
      Serial.print("payload[0] : ");
      Serial.println(payload[0]);
 
      delay(INTERVAL_VS_BASE);
      adjust_base();
      if (payload[0] == '1') {
        delay(INTERVAL_VS_BASE);
        diningServo.write(forward);
        delay(INTERVAL_VS_BASE);
        diningServo.write(base);
        delay(INTERVAL_VS_BASE);
      } else if (payload[0] == '2') {
        delay(INTERVAL_VS_BASE);
        diningServo.write(backward);
        delay(INTERVAL_VS_BASE);
        diningServo.write(base);
        delay(INTERVAL_VS_BASE);
      } else if (payload[0] == '3') {
        kitchen_light_on();
      } else if (payload[0] == '4') {
        kitchen_light_off();
      } else if (payload[0] == '5') {
        delay(INTERVAL_VS_BASE);
        diningServo.write(base);
        delay(INTERVAL_VS_BASE);
        ESP.restart();
      }
      setOutput(9);
      delay(INTERVAL_VS_BASE);
      adjust_base();
      delay(INTERVAL_VS_BASE);
 
      break;
  }
}
 
void adjust_base() {
  delay(INTERVAL_VS_BASE);
  diningServo.write(base);
}
 
String formatBytes(size_t bytes) {
  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) {
  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";
}

 なお、気づけば、スケッチはESP8266のままとなっていますが、数年前からESP32、これによりライブラリもESPmDNSに替えてあり、Androidデバイス対策としてIPも固定、なぜか使えなくなったESP32WebServerにも対処するなど所々、変更を加えてあります。

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 src="WebSocket.js" type="text/javascript"></script>
-->
<script>
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);
}
</script>
</head>
<body>
<center>
<header>
<h1>ダイニング&キッチンライト</h1>
</header>
<div class="ctrlbtn"><input id="don" type="submit" onclick="sendCtrl('1');" value="ダイニングライトON" style="width:160px ;"></div>
<div class="ctrlbtn"><input id="doff" type="submit" onclick="sendCtrl('2');" value="ダイニングライトOFF" style="width:160px ;"></div>
<div style="clear:both ;"></div>
<div class="ctrlbtn"><input id="don" type="submit" onclick="sendCtrl('3');" value="キッチンライトON" style="width:160px ;"></div>
<div class="ctrlbtn"><input id="doff" type="submit" onclick="sendCtrl('4');" value="キッチンライトOFF" style="width:160px ;"></div>
<div style="clear:both ;"></div>
<div><input id="data" type="text"></div>
<div><input type="button" name="mainmenu" value="to Main Menu" onClick="http_req(location.href='http://esphamainsrv.local')"></div>
</center>
</body>
</html>

 なぜか、外部CSSは適用されるのに外部JavaScript(WebSocket.js)は適用されなかったのでJavaScript部分は、HTMLに埋め込みました。

 押下ボタンを判別、送信するメソッド・関数sendCtrl(btn){}以外は、参照させて頂いたプロジェクト同様です。

 テキスト(input type=text)は、外部スクリプトでうまくいかなかった際のボタン押下時のvalue値確認用です。

 index.htmlのhead内は、必要な行のみ使い、外部CSSにも手は加えましたが、ちょっと雑さが目立ちますし、なくても機能確認には十分なので割愛します。

操作方法

ESP8266+サーボ自作ダイニング・キッチン各ライトスイッチ用ブラウザ版操作パネル

 ブラウザにmDNS名でmDNS.local/index.htmlか、IPアドレスで***.***.***.***/index.htmlにアクセスすると、ダイニング用ライト、キッチン用ライトそれぞれのオン・オフ操作パネルが表示されます。

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

 押下ボタンがわかると言えばわかるのでテキストボックスである必要はないものの、確認用なのであってもなくても可。

スクリプトから操作

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

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

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

2020/04/27

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

2020/12/18

 Python/pip/websocket周り仕様変更!?にハマる参照。

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

 また、先の照明スイッチ用含め、ブラウザ版操作パネルをデスクトップ版操作パネルから呼ぶこともできます。

ホーム前へ次へ