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

使用中のカーテンを無線電動化 ESP8266・ESP32/WebSocket

ホーム前へ次へ
ESP8266って?

使用中のカーテンを無線電動化 ESP8266・ESP32/WebSocket

使用中のカーテンを無線電動化 ESP8266・ESP32/WebSocket

電動部自作カーテン
2020/04/23

 Wi-Fi(wifi)モジュールESP8266/ESP32の内、ESP32開発ボードを使って無線で開閉できる電動カーテン(スマートカーテン?)を作ってみるページ。

 以前、作ったArduinoとステッピングモータ28BYJ-48で既存のカーテン電動化を無線化したもの。

 WiFi操作や音声操作もできますが、実は、まだ仕掛かり中で検討中の手動開閉もできるようにすることとモータ周りの回路の配置・固定、自作カーテンボックス設置が残っています。

 無線版としては、MQTTもありですが、今回は、操作にあたり、HTML5実装のWebSocketを介すことにしました。

 MQTT同様、非同期通信できますが、MQTTと違ってWebSocketには、ブローカーといったものは、存在しません。

 端末(≒ターミナル)からではなく、ブラウザから手軽にできるのでPCでもスマホやタブレットでも操作可能、後述のようにスクリプトでも対応できるため、デスクトップ版操作パネルからブラウザ操作パネルを表示したり、スマートスピーカー(自身の場合は、自作Julius/Open JTalk/ラズパイスマートスピーカー)から音声操作することもできます。

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

[2020/04/30] ESP32内蔵時計+NTPを利用して自動開閉とブラウザからの時間設定も可能な自動タイマー付き無線電動カーテンにしてみました。

前置き

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

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

 とても便利なAruduinoOTAですが、1点だけ、注意すべきことが...。

 既にAruduinoOTAを使ったIoTガジェットを運用中の場合にArduino IDEでArduino、ESP8266やESP32などのスケッチを書いてアップロードする場合、シリアルポートの設定に気をつけないと、たまたま、ESP8266やESP32などボードは一致しつつも、シリアルポートに違うOTAアドレスが指定されたまま、アップ...、対象とは違うIoTガジェットのスケッチを書き換えてしまった...なんてことも...。

 すぐに気づけば、それぞれに相応のスケッチをアップすれば済むので、まだ良いのですが、すぐに気づかなかった時、あれ?機能しない、なんで!?などと右往左往...というわけで気をつけましょう。

使ったもの

 タミヤのギアボックスも4つほど持っていますが、静音と速度のバランスでというか、静音重視で最初から使えるものならステッピングモータ、手持ちのものを、検索しても誰もカーテンには使っていない模様ということで以前のArduino版カーテン時点から28BYJ-48 5Vを採用することにしました。

 何れも全てのパーツを使うわけではないものの、ラダーチェーン4.5セットとユニバーサルアームセットx1で約3500円と半分以上を占め、国内ではカップリングも結構することもあり、国内価格で5000円前後、Aliexpressで7割程度でしょうか。

 今回は、5Vのステッピングモータで挑もうとしており、開閉時カーテン自体の滑りもスムースであることに越したことはなく、今回の検証同様、必要に応じて次のようなものも必要になるかもしれません。

 ちなみに今回は、リミットスイッチをONにすべく、ラダーチェーン上にブルタック/Blu・Tack(粘着ラバー)で使用済みのインシュロック(結束バンド)の頭を埋め込みました。

 また、カーテンを引っ張るにあたり、ラダーチェーンとレールランナー間を接続するパーツをプラバンで製作しました(製作と言ってもカッター、定規になるもの、ろうそくがあれば十分)。

 初期の試行錯誤からここまでについては、ラダーチェーン&スプロケットセット購入レビューとその後、続いてArduino/ステッピングモータ28BYJ-48/ラダーチェーンで既存のカーテンを電動化を参照。

前提

 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の環境設定で追加する方法の場合、カンマ区切りで複数指定可能。

回路

 回路については、ほぼ、Arduino版のArduinoが、ESPボードに変わっただけです。

 今後、手動開閉もできるようにする予定で構想としては、開・閉動作完了後に動力を切り離すつもりなので手で開閉した際にモータに負荷がかかることはありませんが、これを実装しない(できない)場合、電動化を実施する場合には、整流ダイオードが必要になるでしょう。

 また、後述のように操作は、物理スイッチではなく、後述の通り、同じネットワーク上にいるPCやタブレット、スマホなどのブラウザを介し、遠隔操作するだけです。(冒頭述べたように音声操作もできます。)

電源

 既存のカーテンの開閉を円滑にする工夫を含め、試行錯誤した結果、電源には、十分な電流を供給できるACアダプタを入力、極限までは未確認も、これを降圧して使うことで無負荷時5.3V台(運転時約4.9V〜を維持)程度でもESPボードのVIN/GNDとモーター用5V/GNDを共用して機能します。

 ちなみにWiFiにおける消費電流が影響するのだと思いますが、Arduinoと同一条件でUSB延長ケーブルを介しただけでWiFi接続できず、ESP上のWebサーバにアクセスできず、ESP32にUSB延長コード+microUSBケーブルから電源供給しつつ、モーターに別電源を使った検証はできませんでした。(電源タップの延長コードを使えばいけるでしょうが。)

スケッチのアップロード

 初めてArduinoOTAを使うボードの場合、AruduinoOTAサンプルスケッチBasicOTAの、既に他スケッチで機能していた場合、OTA用の内容を網羅したオリジナルスケッチをESP32開発ボードやESP8266 NodeMCUボードなどにアップロード。

 こうすることでOTAアップデートできるようになります。

スケッチ

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

// ESP32版電動カーテン
// Base Code
// 無線電動自作ロールスクリーン
// ペンダントライト用WiFiサーボON/OFFスイッチ
 
#include <WiFi.h>
#include <WiFiClient.h>
#include <ArduinoOTA.h>
#include <ESP32WebServer.h>
#include <ESPmDNS.h>
#include <SPIFFS.h>
#include <Arduino.h>
#include <FS.h>
#include <WebSocketsServer.h>
#include <Stepper.h>
 
const char* path_root  = "/index.html";
 
const char *ssid   = "SSID";
const char *password = "PASSPHRASE";
 
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
 
ESP32WebServer server ( 80 );
WebSocketsServer webSocket(81);     // WebSocketデフォルトポート 81
 
File fsUploadFile;
 
const char common_name[40] = "esp32_my_curtain";
const char *OTAName = common_name;   // OTAサービス名
const char *mdnsName = common_name;   // mDNS用ドメイン名
 
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 LimitSW = 26;
 
const int stepsPerRevolution = 200;   // 1回転あたりのステップ数
int motorSpeed = 1200;         // 数値が小さいほど高速度、大きいほどトルク大
int lookup[9] = {B01000, B01100, B00100, B00110, B00010, B00011, B00001, B01001, B00000};
 
Stepper myStepper(stepsPerRevolution, motorPin1, motorPin2, motorPin3, motorPin4);
 
void setup() {
 delay(1000);
 Serial.begin(115200);
 delay(10);
 Serial.println("\r\n");
 
 //declare the motor pins as outputs
 pinMode(motorPin1, OUTPUT);
 pinMode(motorPin2, OUTPUT);
 pinMode(motorPin3, OUTPUT);
 pinMode(motorPin4, OUTPUT);
 pinMode(LimitSW, INPUT_PULLUP);
 
 startWiFi();
 startOTA();
 startSPIFFS();
 startWebSocket();
 startMDNS();
 startServer();
}
 
void loop(){
 webSocket.loop();              // constantly check for websocket events
 server.handleClient();           // run the server
 ArduinoOTA.handle();            // listen for OTA events
 // ESP32用light-sleepモード設定(自動復帰?)
 // esp_light_sleep_start();
}
/*__________________________________________________________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");
 /*
  if (!readHTML()) {
   Serial.println("Read HTML error!!");
  }
 */
 /*
  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);
    //    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]);
 
   if (payload[0] == '1')
   {
    Serial.print(" clockwise loop: " );
    while (1) {
     if(!digitalRead(LimitSW)) break;
     counterclockwise();
     delayMicroseconds(motorSpeed);
     Serial.print(" ");
     delay(1);
    }
    Serial.print("]");
    setOutput(9);
   }
   else if (payload[0] == '2')
   {
    Serial.print(" counterclockwise loop: " );
    while (1) {
     if(!digitalRead(LimitSW)) break;
     clockwise();
     delayMicroseconds(motorSpeed);
     Serial.print(" ");
     delay(1);
    }
    Serial.print("]");
    setOutput(9);
   }
   else if (payload[0] == '0')
   {
    setOutput(9);
   }
   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 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 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));
}
 
/*__________________________________________________________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);
}

 WebSocket communicationプロジェクトのスケッチとこれまで使ってきたスケッチの合作です。

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

 WebScokets.hは、複数のライブラリで利用可能なようですが、今回は、arduinoWebSocketsライブラリを使用しました。

 このスケッチ、ブラウザ上でのスケッチやSPIFFS内ファイルのアップデート機能までありますが、これらについては未検証で、ここでは、Arduino IDEからOTAアップデートしました。

 マルチアクセスポイント対応でアクセスポイントモードでしたが、ステーションモードに変更しました。

 なぜか、元のスケッチだと4つの内、一部は、モータードライバのLEDが点灯するものの、全点灯せず、モーターが回わらなかったため、既に動作確認がとれているスケッチからコードを取り込みました。

 モーターを回すためのループが完了した後もモータードライバ上のランプが点灯したままとなるため、これを消灯させるべく、停止を入れています。

 ESP8266ボードを使う場合、ESP32と異なり、SPIFFSを利用する場合でもSPIFFS.hは不要、WiFi.h、ESP32WebServer.h、ESPmDNS.hに代えて、それぞれ、ESP8266WiFi.h、ESP8266WebServer.h、ESP8266mDNS.hをinclude、これに伴い、ESP32WebServer server(80);は、ESP8266WebServer server ( 80 );に変更、モーター用出力ピンには、ESP8266相応のピンを指定、また、loop()内のスリープ関数もESP8266用にする必要があります。

 それ以外でコンパイルが通らない場合、きっと、コメントアウトでしのげるでしょう...。

 ちなみにそのままだと.gzファイルにも対応しているため、それを残したまま解凍・展開して編集しても反映されない!と慌てないよう要注意。

HTML/JavaScript

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>自室カーテン操作パネル</title>
  <link href='main.css' rel='stylesheet' type='text/css'>
  <meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport'>
<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("num").value = btn
  console.log('Btn Num: ' + btn);
  connection.send(btn);
}
</script>
</head>
 
<body>
  <center>
  <header>
  <h1>自室カーテン操作パネル</h1>
  </header>
  <div "><input id="open" type="submit" onclick="sendCtrl('1');" value="開ける" style="width:80px ;"></div>
  <div "><input id="close" type="submit" onclick="sendCtrl('2');" value="閉める" style="width:80px ;"></div>
  <div><input id="num" type="text"></div>
 
  </center>
</body>
</html>

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

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

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

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

操作方法

ESP32電動カーテン用ブラウザ版操作パネル

 ブラウザにmDNS名でmDNS.local/index.htmlか、IPアドレスで***.***.***.***/index.htmlにアクセスすれば、UP、STOP、DOWNボタン付きの操作パネルが表示されます。

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

 流用させて頂いたスケッチについては、アクセス時index.htmlを省略する方法がいまいちわかりませんでした...。

 テキストボックスは押下ボタン確認用なのでなくても構いません。

スクリプトから操作

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

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

 このようにスクリプトから操作できるということは、自作スマートスピーカーからでも操作できます。

 というわけでブラウザからのWiFi越しの無線操作に加え、自作ラズパイスマートスピーカーから電動部自作カーテンを音声操作できるようにしてみました。

2020/04/27

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

2020/12/18

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

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

 また、このカーテン用含め、ブラウザ版操作パネルを冒頭触れたデスクトップ版操作パネルから呼ぶこともできます。

備考

 非同期通信なのでスクリーン動作中であってもHTTP TCP/IP同期通信のようにフリーズすることはないものの、今のところ、割り込み方法を思いつかないのでブラウザ上のボタン操作(や自作スマートスピーカーからの音声操作)による途中での逆転や停止は想定していません。

 ただ、端末で操作したMQTTと異なり、プログラム上、開/閉後に停止を入れているからか、なんだかカッコいい気もする一方、意味を見いだせるのか見い出せないのか、非同期の副産物として[開ける]ボタン、[閉める]ボタン(逆も真なり)を続けてクリック・タップすると全開→全閉(全閉→全開)を自動で行ってくれます。

 ということは、間違えて続けて押してしまうと開けたつもりなのに閉まってる!とか、その逆もあり得るという話。

 が、開/閉をそれぞれ続けてクリック・タップしても直後にメインメニューに戻る(ロールスクリーンのメニューを抜ける)と最初の動作だけ行って2つめ含む以降の動作はしないことがわかりました。

 カーテンのメニューも各操作もカーテン用に割り当てたESPチップのフラッシュメモリ上に展開されたパスにアクセスすることで表示・操作できるようになっており、フラッシュメモリ内にアクセス中は、操作における値を保持、アクセスしなくなる(=カーテン用に割り当てたESPチップのスケッチを抜ける・スケッチの範囲外になる)と即、値も破棄されるということなのでしょう。

 今回は、リミットスイッチで開閉を停止させているので空振りでもしない限りは、開けすぎ、閉めすぎの心配はありません。

ホーム前へ次へ