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

自動開閉タイマー付き無線電動カーテン 自作スマートカーテン

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

自動開閉タイマー付き無線電動カーテン 自作スマートカーテン

自動開閉タイマー付き無線電動カーテン 自作スマートカーテン

2020/04/30

 Wi-Fi(wifi)モジュールESP8266/ESP32の内、ESP32開発ボードとステッピングモータ28BYJ-48を使って自動開閉タイマー付きの無線で開閉できる電動カーテン(スマートカーテン)を作ってみるページ。

 先日、作った後付けできる無線電動化カーテン ESP8266・ESP32/WebSocketにブラウザからデフォルトまたは、別途時間指定も可能な自動開閉タイマーを追加したもの。

 もちろん、スマホ、タブレット、PCなどのブラウザから、また、自作ラズパイ/Julius/Open JTalkスマートスピーカー同スマートスピーカー機能を搭載したPCからの音声操作もできます。

 今回、カーテンは、1枚もので同時に両サイドに向かってそれぞれ閉じ、両サイドからそれぞれ内側に開く(真ん中で束ねる)ようにしましたが、後述の自作パーツ次第でカーテンは1枚ものでも2枚ものでも開閉方向は、どうにでもなります。

 手動開閉と自作カーテンボックスについては仕掛かり中、今回は、WebSocketもMQTTも使い(使え)ませんでした。

 => [2020/05/03] 自作カーテンボックスできました。

 => [2020/05/08] 電動・手動切り替えクラッチのプロトタイプを作ってみました。

 ちなみに動画のPC画面上のカーテン操作パネル右横に出ている時計は、当サイトのページ

ちなみに

 前置き、使ったもの、初期の試行錯誤からここまでについては、ラダーチェーン&スプロケットセット購入レビューとその後、続いてArduino/ステッピングモータ28BYJ-48/ラダーチェーンで既存のカーテンを電動化、更に使用中のカーテンを無線電動化 ESP8266・ESP32/WebSocketを参照。

プラダン/リメイクシート製自作カーテンボックス
[2020/05/03]

 今日、プラダン/プラスチックダンボール、固定用のアングルダイソーのリメイクシートを買ってきて早速、カーテンボックスを作ってみました。

 プラダン1820x910x4mm 1枚1188円、リメイクシート 200x15cm 3枚330円、アングル5個520円分買いましたが、プラダンは、内1820x200mm、リメイクシートは内1枚弱、アングルは内2個しか使っていません。

 加えて、手動開閉機能をどうするか決まれば、それ込みで後日回路・モータ・リミットスイッチ周りを定位置に固定の上、この部分(おそらくプラダンの横500〜600mm、縦400〜600mm、リメイクシート700〜800mmx150mm程度、アングル1個)のカーテンボックスも追加製作、アングルとカーテンボックスの固定もするつもりですが、それにしても実際には、約500〜600円の追加ですかね。

 今、アングルとカーテンボックスは、先のブルタックでぺたっと貼ってあるのですが、これだけでもいいかな...。

 それにしてもまるで最初から付いていた木製カーテンボックスかっていうくらい近くで見ても違和感なしなし、これで500〜600円なら、超満足。

 あ、むしろ今どきの人類は、木目プリントに慣れ過ぎてるからかも?微妙な気もするけど、まぁ、それでもいいよね、満足なんだから。

[2020/05/04] いい感じだったので追加で電動にしていない部屋にも同じく白のプラダンをベースに付けてみたのですが、陽の射し込み具合によっては、裏面の貼り様が、表面に映り込むのでプラダンの色は、黒とか濃い系の方がよかったかも...
 固定は、ステー穴に位置を合わせてプラダンに穴をあけ、ボルト、ワッシャー、ナットで留めましたが、なかなか良いです。

[2020/05/08]

 手でも開けられるように歯車でクラッチっぽいものを作ってみました。

 使った歯車は、67種類入ったプラスチック製ギアパックから選んだもの。

 ラック&ピニオン、ボールねじ、クランク&スライダなどのリンク機構を使って...といろいろ思考を巡らせましたが、こじんまりと省スペースで手間をかけずに簡単に手持ちのもので作りたい...ということで、安易にサーボモータのスイングでギアを脱着してみました。

 NO/Normal Open/常時開のリミットスイッチが押される(CLOSEする/閉じる)とステッピングモータが止まり、サーボモータで接続している(仲介している)歯車を跳ね上げ、動力を切り離す...と思い描いた通りに動いてくれました。

 が...トルクが弱い気が...っていうか、負荷がかかると滑っちゃうというか、歯飛びしちゃうというか...厚さがもっと厚い方がよいのか...、溝が深い方がよいのか...、材質が硬い方がよいのか...、そもそもサーボのホーンも硬いものを...いや、ステッピングモータにした方が...。

 安定性とか考えるとイレギュラー過ぎるかな...移動するにしても、直線運動じゃないとダメかな...っていうか、歯車(ギア)について無知なのに作っちゃいけないよね...やっぱり、ちゃんと学ばないとダメか...。

[2020/05/09]

 あ、トルクは、回転体と同じスプロケットを平歯車として使うことで維持したまま、うまく動力伝達できました...そりゃそうですよね。

 が、3個同じ大きさや仲介するギアのみ小径のスプロケット(計5セット買ったタミヤ ラダーチェーン&スプロケットセットのもの)を使ってみており、他は固定、仲介・空転するスプロケットのみ、軸に通して手で持ってやってみていますが、向きによってギアが、うまく、かみあい回りやすかったり、相当な負荷がかかりつつ、回りながらも時にがっつりハマって回らなくなったり...カーテンで言えば、閉めるのは割とスムースも開ける時は、めちゃめちゃ手に負荷がかかる...。

 これまでの電動開閉の過程でこんなことはなかったのに、なぜ?やっぱり、チェーンと組み合わせて使うスプロケットは、そもそも平歯車としては使えない?

 っていうか、豊富な中から材質、径や厚さ、歯数なんかを選べて...みたいな歯車ってどこかに手頃な値段で売ってるんだろうか...?もしかして、そんな時は、レゴのギアとか使うのかな!?

 ちなみにベースは、タミヤのユニバーサルプレートやユニバーサルアームのパーツ、ステッピングモータ共々、モータ側のスプロケット台もこれに固定しています。

[2020/05/11]

 うーむ、歯車のバラエティセットみたいなものやタミヤの四駆用ギアは薄くて小さそう、レゴの歯車は良さげも材質はともかくシャフトが十字の特殊なものっぽく汎用性はなさ気。

 各種歯車を1個から製作してくれるというサイトはさすがに敷居が高そう、ならばとAmazonに小原歯車(KHK)や協育歯車工業(KG Gear)製のギアがあるのは、知っていたものの、玄人っぽすぎると思って詳細を確認していませんでしたが、こうなったら確認してみようと思うに至りました。

 前者は、簡潔ながらもAmazon内の商品説明の情報量が多く、ざっとネット検索しても小難しい、似たような情報が多い中、歯車技術資料を公開してくれており、歯だけに?ものすごく、わかりやすく噛み砕いた説明資料が各種、PDFやHTMLとして提供されていて、まだ、流し読みしかしていませんが、相当勉強になっています。

 そんなこんなもあって小原歯車(KHK)製の樹脂製ギアを買ってみようかと思っているのですが、仲介ギアはともかく、入出力ギアの歯数を同一にするとしても、このタミヤのラダーチェーン&スプロケットにモーター直と同じ程度の速度・トルクを維持して動力を伝達するにあたり、選定方法がまだ理解できていません。

 穴径は、スプロケットやステッピングモータの軸径、手持ちのカップリングの径からして3mmや5mmが都合がよい、素材は樹脂、種類は平歯車にしようと考えています。

 偏摩耗を避けるため、できれば歯数が素数になる対にした方がよいということで仲介ギアは相応の小径にする、歯形(一般的なインボリュート歯形の他、サイクロイド歯形...etc.)、アンダーカット防止の為などの噛み合い圧力角はともかく、基準圧力角(一般的な20度の他4.5/15/17.5度...etc.)を合わせる、モジュール(歯の大きさ)が同じギアの組み合わせる、プラスチック製ギア同士だと熱を持ち、膨張しやすい為、できるなら他方を金属製にする等々...が良さそうなことはわかりました。

 が、歯の厚さ(歯幅)と基準円直径もしくは、歯先円直径さえ、スプロケットと同じくらいなら、うまく伝達できるんだろうか?スプロケットと同じかそれくらいのモジュールや歯数が良いのだろうか?全ての条件が一致するものなんてあるのだろうか?なかった場合、選定にあたり、何を基準にしたらよいのだろうか?

 試しにスプロケットと歯先円直径と歯幅が近いものをモジュール、穴径で絞ってAmazonで買ってみようと思ったら、直径を変えても、在庫なしや入荷予定はあるものの、今時点1点のみとか...で見送り。

[2020/05/15]

 DS1-28はもう1個欲しいところですが、とりあえず、DS1-15、DS1-28 各1個をAmazonで買ってみました。

[2020/05/27]

 ネット上をいろいろ探し回って勘案した結果、自身初マルツでDS1-28を発注してみることにしました(納期4日間予定)。

[2020/05/29]

 思った以上に早くマルツに注文してあったDS1-28が届いたので、早速、空転する径のシャフトに中継となるギアDS1-15、両サイドにシャフト径ピッタリのスプロケットを挿し、手で持って着脱してみました。

 想定通り、概ね良好に開閉できるようになったものの、スプロケットを平歯車として試した時とは逆に開ける時ではなく、カーテンを閉め切るちょっと前くらいから負荷が大きくなっているようで仲介ギアが、モーター側とチェーンスプロケット側のギアに持っていかれ、ギアが噛んだ状態になります。

 まずは、この負荷が大きくなる原因の究明と併行して多少の負荷に耐え得る中継ギアの固定方法の検討、ひいては、他のギアもというか、装置の土台としているプレート(タミヤ ユニバーサルプレート)の材質変更の可能性も含め、固定方法を再検討する必要がありそうです。

 これに伴い、負荷が大きくなることが不可避なら、どうするにしてもサーボを使う方法だとホーンが耐えられない・耐えても不安定になりそうです。

 よって着脱機構にも他のモータを使うこと、我ながら邪道と思えるスイング式ではなく、やはり、部品在庫となっているSOTEC e-oneとか、TOSHIBA dynabook Satellite T30 160C/5W、はたまた、HP Pavilion Slimline s3140jpなどのFDやCD/DVDドライブのステッピングモータ&ボールねじ機構、または、回転角度を正確に指定できる、静音であるという点で購入済みでカーテン開閉の動力と同じステッピングモータ28BYJ-48と、やはり、購入済みのタミヤ スライドアダプターによるスライダ・クランク機構やタミヤ ラック&ピニオンによる直線運動方式などとの併用を検討、試してみることになりそうです。

 ちなみに現在、カーテンは、チェーンを付けたまま、手で開閉しており、コスパを考えるとアレですが、前述の通り、中央で束ねる恰好で何れか一方の開閉で他方も開閉できるのは、これだけでも十分かなと思ってしまうほど超絶便利です。

 電動なら無音に近いと言えるほど静音なカーテン開閉も手動開閉だと28BYJ-48のような優雅な動作にはならない(えてして豪快に開閉してしまう)のでそれなりに牽引工作物一体のラダーチェーン動作音がしますが、これは、意識してゆっくり開閉するか、プーリー式に変更でもしない限り、致し方ないですね。

前提

 前提については、冒頭リンクの先代と同じです。

回路

 回路については、冒頭リンクの先代と同じです。

電源

 電源については、冒頭リンクの先代と同じです。

OTAにおけるスケッチのアップロード

 OTAについても冒頭リンクの先代と同じです。

スケッチ

 今回、自動開閉タイマー機能を搭載するにあたり、前回のWebsocket版とは、新規と言えるほど、大幅にスケッチを書き換えました。

// ESP32版電動カーテン
// 強いて言えばBase Codeはこれ
// ESP8266/ESP32東芝エアコン大清快をWiFi操作
 
#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 <Stepper.h>
 
const char* path_root   = "/index.html";
 
const char *ssid     = "SSID";
const char *password = "PATHPHRASE";
 
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
 
ESP32WebServer server ( 80 );
 
File fsUploadFile;                                    // a File variable to temporarily store the received file
 
const char common_name[40] = "esp32curtain";
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
 
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;
int motorSpeed = 1200; //速度。数値が小さいほど速くなる。800以下は脱調して動かない
//int lookup[8] = {B01000, B01100, B00100, B00110, B00010, B00011, B00001, B01001};
int lookup[9] = {B01000, B01100, B00100, B00110, B00010, B00011, B00001, B01001, B00000};
 
Stepper myStepper(stepsPerRevolution, motorPin1, motorPin2, motorPin3, motorPin4);
 
// 時刻編集・設定用
String opentime = "";
String closetime = "";
String open_hour = "";
String open_minute = "";
String close_hour = "";
String close_minute = "";
String set_open_hour = "";
String set_open_minute = "";
String set_close_hour = "";
String set_close_minute = "";
// 自動タイマー既定時刻用
String default_open_hour = "07";
String default_open_minute = "30";
String default_close_hour = "18";
String default_close_minute = "00";
// 自動タイマー作動中フラグ
// delayの影響か、秒が飛び評価できず、
// 分で評価するとリミットスイッチを押しても
// 1分間は復帰してしまうため苦肉の策。
boolean onflg = false;
 
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;
}
 
void handleRoot() {
  Serial.println("Access");
 
  String valueString;
  String delimiter = "/";
  int pos1, pos2, pos3;
  int slashpos;
 
  char message[20];
  String(server.arg(0)).toCharArray(message,20);
  server.send(200, "text/html", (char *)buf);
 
  if(server.arg(0).indexOf("OPEN") != -1){
    Serial.println("OPEN");
    curtain_open();
    setOutput(9);
  }
  else if(server.arg(0).indexOf("CLOSE") != -1){
    Serial.println("CLOSE");
    curtain_close();
    setOutput(9);
  }
  else if(server.arg(0).indexOf("SET") != -1){
    Serial.println("SET");
    opentime = "";
    closetime = "";
    int slash1 = server.arg(0).indexOf(delimiter);
    int slash2 = server.arg(0).indexOf(delimiter, slash1 + 1);
    if (slash1 >= 0 && slash2 < 0) {
        opentime = server.arg(0).substring(slash1);
    } else if (slash1 >= 0 && slash2 >= 0) {
        opentime = server.arg(0).substring(slash1 + 1, slash2);
        closetime = server.arg(0).substring(slash2 + 1);
    }
    if (set_timer(opentime, closetime)) {
      Serial.print("タイマー設定済み");
    } else {
      Serial.print("タイマー設定エラー");
    }
  }
 
}
 
boolean set_timer(String time1, String time2) {
   Serial.println("set");
 
  String colon = ":";
  int colonpos1, colonpos2;
  colonpos1 = time1.indexOf(colon);
  colonpos2 = time2.indexOf(colon);
  if (time1 != "") {
    open_hour = time1.substring(0,colonpos1);
    open_minute = time1.substring(colonpos1 + 1);
  }
  if (time2 != "") {
    close_hour = time2.substring(0,colonpos2);
    close_minute = time2.substring(colonpos2 + 1);
  }
 
  set_open_hour = set_open_minute = "";
  if (open_hour != "") {
    set_open_hour = open_hour;
    set_open_minute = open_minute;
  }
 
  set_close_hour = set_close_minute = "";
  if (close_hour != "") {
    set_close_hour = close_hour;
    set_close_minute = close_minute;
  }
 
  return true;
  server.send(200, "text/html", "set");
}
 
void handleNotFound() {
 
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
 
  for ( uint8_t i = 0; i < server.args(); i++ ) {
    message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n";
  }
  server.send ( 404, "text/plain", message );
}
 
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 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.on("/edit.html",  HTTP_POST, []() {  // If a POST request is sent to the /edit.html address,
    server.send(200, "text/plain", "");
  });
  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.");
}
 
void clockwise()  //時計回り
{
  for (int i = 7; i >= 0; i--)
  {
    setOutput(i);
    delayMicroseconds(motorSpeed);
  }
}
 
void counterclockwise()  //反時計回り
{
  for (int i = 0; i < 8; 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));
}
 
void curtain_open() {
  Serial.println("open");
  while (1) {
    if (!digitalRead(LimitSW)) break;
    clockwise();
    delayMicroseconds(motorSpeed);
    delay(1);
  }
  server.send(200, "text/html", "open");
}
 
void curtain_close() {
  Serial.println("close");
  while (1) {
    if (!digitalRead(LimitSW)) break;
    counterclockwise();
    delayMicroseconds(motorSpeed);
    delay(1);
  }
  server.send(200, "text/html", "close");
}
 
void autotimer() {
  // ESP32内蔵RTCを使用・setup()でNTPと併用
  time_t t;
  struct tm *tm;
  static const char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};
  char rdate[30], rday[30], rtime[30], rsec[30];
  char rhour[30], rminute[30];
  int irhour, irminute, irsec;
 
  t = time(NULL);
  tm = localtime(&t);
 
  Serial.printf(" %04d/%02d/%02d(%s) %02d:%02d:%02d\n",
                tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,
                wd[tm->tm_wday],
                tm->tm_hour, tm->tm_min, tm->tm_sec);
 
  sprintf(rdate, "%04d/%02d/%02d",
          tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
  sprintf(rday, " (%s)",
          wd[tm->tm_wday]);
 
  sprintf(rtime, " %02d:%02d",
          tm->tm_hour, tm->tm_min);
  delay(1000 - millis() % 1000);
 
  sprintf(rhour, "%02d", tm->tm_hour);
  delay(1000 - millis() % 1000);
 
  sprintf(rminute, "%02d", tm->tm_min);
  delay(1000 - millis() % 1000);
 
  sprintf(rsec, "%02d", tm->tm_sec);
  delay(1000 - millis() % 1000);
 
  irsec = atoi(rsec);
 
  if (set_open_hour == "") {
    set_open_hour = default_open_hour;
    set_open_minute = default_open_minute;
  }
  if (set_close_hour == "") {
    set_close_hour = default_close_hour;
    set_close_minute = default_close_minute;
  }
 
    // 既定または、指定時刻に開く
    if (String(rhour).equals(set_open_hour)
      && String(rminute).equals(set_open_minute)
      && irsec <= 3) {
      curtain_open();
      setOutput(9);
    }
    // 既定または、指定時刻に閉める
    if (String(rhour).equals(set_close_hour)
      && String(rminute).equals(set_close_minute)
      &&  irsec <= 3) {
      curtain_close();
      setOutput(9);
    }
}
 
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();
  startMDNS();
  startServer();
 
  // NTP start
  configTime( 9 * 3600L, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
}
 
void loop() {
  server.handleClient();
  ArduinoOTA.handle();                        // listen for OTA events
 
  autotimer();
}

 オートタイマーには、ESP32内蔵時計とNTPを併用、デフォルト時間の他、ブラウザ上で時間を設定することもできるようにしました。

 値(設定時間)のためには、SPIFFS領域を使っていないのでESP32のリセットでデフォルトに戻ります。

 何回なのか具体的には忘れましたが、書き換え回数に制限があると思うと、それが仮に1万回だったとしても、なんとなく避けたくなって...それに電源を落としたり、リセットしたりしなれば保持できますし、仮にリセットしたとしても大勢に影響はないかなと。

 後述のHTML+CSS+JavaScriptでクリック・タップしたボタンや、これに応じて取得したデータによって制御(まぁ当然か)。

 [開ける]/[閉める]ボタンについては、それぞれ開・閉するだけ、[クリア]ボタンによるデフォルト値含め、[タイマー設定]で受け取った時間を使って自動開閉タイマーを設定(これも当然か...)。

 綱渡りっぽい工夫を要したのは、開閉時間の条件となる"秒"ですかね。

 delayなどの影響なのか、1秒毎に"秒"をとれなかったための苦肉の策、これをやらないとリミットスイッチを押せば止まるものの、押していないと1分間は、回転し続けてしまうので。

HTML/JavaScript

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1" />
<title>自室電動カーテン</title>
<link href='main.css' rel='stylesheet' type='text/css' />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png" />
<link rel="icon" type="image/png" sizes="144x144" href="/favicon-144x144.png" />
<link rel="icon" type="image/png" sizes="48x48" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#00878f" />
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport' />
<script>
function clearSetTimer() {
  document.getElementById("open_hour").value = "";
  document.getElementById("open_minute").value = "";
  document.getElementById("close_hour").value = "";
  document.getElementById("close_minute").value = "";
  sendCtrl("SET");
}
function sendCtrl(btn) {
var data = "";
  data += btn;
  if (btn == "SET") {
    if (document.getElementById("open_hour").value != "") {
      var openhour = ( '00' + document.getElementById("open_hour").value ).slice( -2 );
      var openminute = ( '00' + document.getElementById("open_minute").value ).slice( -2 );
      data += "/" + openhour + ":" + openminute;
    } else {
      data += "/" + "07:30";
    }
    if (document.getElementById("close_hour").value != "") {
      var closehour = ( '00' + document.getElementById("close_hour").value ).slice( -2 );
      var closeminute = ( '00' + document.getElementById("close_minute").value ).slice( -2 );
      data += "/" + closehour + ":" + closeminute;
    } else {
      data += "/" + "18:00";
    }
  }
  document.getElementById("data").value = data;
  $.ajaxSetup({timeout:1000});
  $.get("/?value=" + data + "&");
  {Connection: close};
}
function http_req(url) {
  var req = new XMLHttpRequest();
  req.open("GET", url, true);
  req.send();
}
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<center>
<header>
<h1>自室電動カーテン</h1>
</header>
<div style="padding:6% ;width:36% ;float:left ;"><input id="open" type="submit" onclick="sendCtrl('OPEN');" value="開ける" style="width:80px ;"></div>
<div style="padding:6% ;width:36% ;float:left ;"><input id="close" type="submit" onclick="sendCtrl('CLOSE');" value="閉める" style="width:80px ;"></div>
<div style="clear:both ;"></div>
<div style="font-size:12px ;">既定オートタイマー/Open 7:30 Close 18:00<br>自動開閉時間を以下で指定できます *1</div>
<div>開ける時間 : <input id="open_hour" type="number" min="0" max="23" style="width:70px ;height:20px ;"> : <input id="open_minute" type="number" min="0" max="59" style="width:70px ;height:20px ;"></div>
<div>閉める時間 : <input id="close_hour" type="number" min="0" max="23" style="width:70px ;height:20px ;"> : <input id="close_minute" type="number" min="0" max="59" style="width:70px ;height:20px ;"></div>
<div style="font-size:12px ;">[0-23]時:[0-59]分指定 / 時を省略すると無効、分を省略すると"0"自動補完。</div>
<div style="padding:6% ;width:36% ;float:left ;"><input id="set" type="submit" onclick="sendCtrl('SET');" value="タイマー設定" style="width:120px ;height:40px ;"></div>
<div style="padding:6% ;width:36% ;float:left ;"><input id="clear" type="submit" onclick="clearSetTimer();" value="クリア" style="width:120px ;height:40px ;"></div>
<div style="font-size:12px ;text-align:left ;">有効な時:分指定後、[設定ボタン]タップで設定可能。<br>未設定の方は、デフォルト(開 07:30/閉 18:00)を自動補完。<br>[クリア]ボタンで時間指定欄の消去及びデフォルト(開 07:30/閉 18:00)自動設定。</div>
<div style="font-size:12px ;text-align:left ;">*1<br>マイコンを再起動するとリセットされ、既定の時間に自動開閉します。</div>
<div><input id="data" type="text" style="width:360px ;height:20px ;"></div>
<input type="button" name="mainmenu" value="メインメニュー" style="width:120px ;height:30px ;font-size:12px ;" onClick="http_req(location.href='http://esphamainsrv.local')">
</center>
</body></html>

 JavaScriptライブラリjQueryのajax機能による、今回は、GETで押下ボタンや時間情報などのデータを送信。

 時間入力ボックスは、form inputのtype属性をnumber、それぞれmin/maxを指定しているため、少なくとも自身愛用のFirefox(68.7.0esr)では、半角数値/全角数字以外、数値範囲外の入力については、当該ボックスを赤枠で囲って注意を促してはくれるものの、別途JavaScriptによる数値チェックや範囲チェックなどで再入力を促すようなことはしていません。

 何気に御用達さないChromium(Chromeオープンソース版)で試してみたら、賢い...、type属性numberだと半角英字や記号のみならず、全角数字さえもそもそも入力を受け付けない...確かにNumberならば、こういう挙動を期待したいところ...ぬぅ...がんばれ!Firefox。

 ちなみに、どちらも[0-99]までを有効数字として扱い、それ以外は赤枠はつくものの、[100]以上とか、[0001]とか、[0205]とか、3桁以上も受け付けつつ、末尾2桁を入力値として扱う、これは仕様通りなのか、何れのブラウザも挙動は同じ模様。

 外部CSSは、いい加減なので、さらしません...CSSはよしなに。

操作方法

ESP32自動タイマー付き無線電動カーテン用ブラウザ版操作パネル

 ブラウザにmDNS名でxxx.localか、IPアドレスでxxx.xxx.xxx.xxxにアクセスすれば、操作パネルが表示されます。

 [開ける]/[閉める]ボタンは、読んだそのままカーテンを開ける/閉める。

 [タイマー設定]ボタンは、時間を省略した場合、無視、分を省略した場合、0補完、開・閉用それぞれの時・分指定ボックス入力値で自動タイマーを設定。

 この時、[開ける時間]/[閉じる時間]において、何れかを省略した場合には、既定の[07:30](前者)、[18:00](後者)を自動補完。

 [クリア]ボタンは、[開ける時間]/[閉じる時間]ボックス内を消去すると同時に既定の[07:30](前者)、[18:00](後者)を設定。

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

 テキストボックスは送信データ確認用なので運用上は、なくても構いません。

 ちなみにこのパネルについては、表示されるまでと表示されてから比較的タイムリーに応答するまでタイムラグが...後者については、画面遷移後、即、開・閉ボタンを押すと一見反応せず、もう1度押すと実は2度機能してしまう...アクセス時の処理が重かった?

 尤も、後者については、まだリミットスイッチを定位置に付けず、手押しして止めているからわかったことで、たとえ、このままにしておいたとしてもリミットスイッチを定位置に設置した後は、仮に2度押ししてもリミットスイッチが機能する為、誤作動にはなりませんが。

スクリプトから操作

$ cat curtain.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-
 
import requests
import sys
 
url = 'http://websockscurtain.local'
arg = sys.argv
urlarg = { 'value':arg[1] }
 
requests.get(url, params=urlarg)
$ chmod +u curtain.py
$ ./curtain.py OPEN
$ ./curtain.py CLOSE
$

 例えば、Python(Python3)だとrequestsを使うと(ESP32のWebサーバに対して)GETやPOSTでURLアクセスできます。

 今回、ESPmDNSを使っているのでIPアドレスではなく、xxx.localとドメイン名でアクセスできます。

 SET(設定)は別途作るとして、sysで引数を取ることができるようにすれば、1つの簡易スクリプトでOPEN / CLOSEできます。

 操作にスクリプトが使えるということは、タイマーなしの先代同様、ブラウザからのWiFi越しの無線操作に加え、自作ラズパイスマートスピーカーから電動部自作カーテンを音声操作することも簡単にできます。

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

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

備考

 タイマー以外の部分で、ここで見当たらなければ、冒頭や後段のリンク先である先代を参照ください。

ウェブ造ホーム前へ次へ