気の向くままに辿る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] 電動・手動切り替えクラッチ/ギア着脱機構のプロトタイプを作ってみました。

 => [2021/05/16] 電動・手動切り替えクラッチ/ギア着脱機構第2弾を作ってみた結果、邪道ながらも成功、残作業はありますが、1年越しに、ほぼ完成しました。

 => [2021/05/19] 動画をアップしつつ、追加機能として電動開閉時に手動開閉したくなった場合などを想定、静電容量式の停止用タッチパネル(タッチ金属プレート)も付けました。

 => [2021/05/25] 電動開閉時、一定時間以上経過で停止機能もでき、一部運用任せな部分と外観の残作業はさておき、完成しました。

 ちなみに動画の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のような優雅な動作にはならない(えてして豪快に開閉してしまう)のでそれなりに牽引工作物一体のラダーチェーン動作音がしますが、これは、意識してゆっくり開閉するか、プーリー式に変更でもしない限り、致し方ないですね。

[2021/05/01]

 ギヤ着脱方法が宙に浮いたまま、思考停止して1年ほど放置していましたが、重い腰を上げて、とは言え、自身には、小型化含め、他の方法の敷居が高く感じるので性懲りもなく、サーボでの着脱を諦めきれず、SG90/MG90Sより仕様上トルクの高いメタルギアMG995金属サーボホーンT25をAliexpressで買って試してみることにしました。

 これらは既に届いたので、もちろん別電源とすれば当然ながら、当初の回路でも単にSG90をMG995と交換しただけで動作自体はしましたが、固定場所等が定まらないこともあり、スイング着脱の検証までには至っていません。

 仮設でやってみようと思ったらちょうどよいカップリングが手元になくAliexpressで3x3mmのカップリングを調達中。

 とりあえず、手持ちの2mmのシャフト、2x3mmのカップリング2個でしのげるかと思いきや、カーテン駆動モータであるステッピングモータ及び構造物と微妙に干渉。

 DS1-15よりも仲介ギアの径を大きくすれば、いけるかな?ということでAliexpressより1日でも早く届けば御の字とばかりにマルツで小原歯車工業の成形平歯車DS1-25とDS1-35を購入、ゴールデンウィーク中なので手配自体GWあけの6日以降となり、仮設+これら歯車でできるかも含め、確認は、これらが届いてからということになります。

 というか、片持ちな上、カップリング2個と2mmシャフト(カップリングを除くむき出し部分が約15mm)分、長くなることでブレが出そうですし、サーボのトルクも十二分に活かせないほか、ベースプレートからちょうどサーボがはみ出す位置になるため、どうやって固定しよう...という感じでとりあえずの確認すらもできるのか未知数ですが。

 まぁ、3mmx3mmのカップリングが届いたところで固定位置は悩ましく、届いてから考えよう...と思っています...。

[2021/05/07]

 DS1-25とDS1-35が届き、前者でも微妙だったので後者で検証。

 やってみると、さすがに金属ギアサーボMG-995、当然ながらプラギヤSG-90より確実に安定感はありますが、実現できるかって言うとできるようなできないような...。

 というのもサーボモータ固定との兼ね合いもあり、不安定な状態で手で抑えつつ、動作確認...という状況では、断言できない...。

 スペース的には、3mmx3mmのカップリングで直につなぐより、むしろ、3x2mmカップリング-2mmシャフト-3x2mmカップリングの方が、明らかに設置位置を確保しやすい感じです。

 が、やはり、片持で歯車までの距離があり、しかもシャフト径が細いこともあり、2つの歯車を仲介するにも、華奢過ぎて、はじかれてしまうケースもあります。

 また、歯車を脱する際に、やや噛んだ状態から無理やり離れる感があることもあるのですが、これが、そもそもの方法論の問題なのか、水平もとれていない不安定な状態でのテストだからなのかも判然としません。

 とは言え、サーボホーンは、弧を描いてスイングする、停止後のステッピングモーター側の歯車は回らない、カーテン側の歯車はフリーでもカーテンの重みなど相応の負荷はある状態なので多少なりとも噛むのは当然でしょう。

 が、サーボを安定して固定・設置できた場合、脱時の挙動が改善するのであれば、程度によっては、より短期間で歯車が削れ、精度が落ち、寿命が縮まるのも覚悟で、強引ながらもアリかなと。

 これが無理ならサーボによる方法は諦めるしかないでしょう。

 ということで、サーボの固定方法にあぐねています...。

 安定させるため、両持ちにする、その場合、サーボと反対側の構造物には、軸が安定する程度の精度でスイング軌道の溝?を設けつつ、軸が溝から抜けないような安定性をもたせる必要があり、それをベースプレートに固定しなければならない...。

 そんなことをする前に片持のまま、スイング軌道を考えた位置にサーボを安定して固定して確認したい...結構シビアで位置決めも難しそう...。

 他の方法は、もっと難易度高そう...。

 技量不足な自身には、果てしない感がなくもない...。

[2021/05/09]

 げっ!バックラッシ(バックラッシュ)ってあるの一方向だけなんだ...。

 ってことは、歯車には向きがあるってことじゃん!

 向き合わせてみたら、一方向は、スムースって言うか、他方がぎこちない気がするじゃん!

 ってか、電動だけならチェーン両端のスプロケット以外の歯車は不要で歯車同士のかみ合わせはないから良いけど、電動・手動切り替え想定して開閉させるのに同じ歯車群を正転・逆転させようとしてる時点でダメってことじゃん!?

 ぽかーーーーーーーーーーーーーーーーーん...。

[2021/05/15]

 ポカンとしてても仕方ないので、とりあえず、バックラッシは置いといてギア着脱方法の第2案を考えてみることにしました。

 180度回転サーボなので半周行って戻ってという感じでしか使わないものの、サーボ+スライダ&クランク機構(スロット&クランクか?)を使い、間にギアを介さず、モータ側をわずかに移動させ、カーテン駆動ギアに直接着脱させてみたらどうなのかと。

 サーボMG995+ホーンT25に加え、スライダ&クランク機構には、タミヤ ユニバーサルプレート用スライドアダプタータミヤ ユニバーサルアームセットバインダー 15 No. 310 PVCに台座としてユニバーサル基板をボルト止めするなどして現在ベースとしているタミヤ ユニバーサルプレート上に配置してみようかと。

 まだ脳内構想段階なのでうまくいくのかは未知数ですが、サーボがダメならステッピングモータとカム状のもので試してみようかなとか。

自作スマートカーテン電動・手動切り替え用ギア着脱方法第2弾
[2021/05/16]

 実際、歯車着脱バージョン2のアイデアを形にしてみたら、駆動ギアの着脱でクラッチ機能相当という強引さとバックラッシ、構造物の寸法的な正確さはさておき、第1案より遥かに安定、位置決めも楽に。

 共に歯車部分をより安定して固定できるに越したことはありませんし、サーボの固定方法は未決ですが、これなら、実現できそうというか、ほぼほぼできています。

 ギア着脱時のみとは言え、サーボ特有のギーッ音はするので気になるのであれば、スケッチ共々、ここでカーテン駆動用に使っている28BYJ-48などのステッピングモータに替えるのもありでしょう。

 が、これまた片持になっていることや荷重のバランスの関係やら何やらでベースプレート(ユニバーサルプレート)に反りが生じているらしく、後方を持ち上げる感覚で手でこれを補正すると良い感じに。

 さて、プレートをより頑強なものにすればよいのか、できれば加工作業は回避したいが、そんな都合の良いものがあるのか、このままでも当て板でもすれば、補正できるものなのか...。

 あ、手でプレートの反りを調整しなくてもスムースに開閉できるようになりました。

 ところどころ微妙に位置を変更したのが功を奏したようです。

 あと、あまり意味はなさ気ですが、カーテンレールとベースプレート(後方、出っ張り方向)の間に当て板として、ちょうどベースプレート端までの長さほどの100均のステンレス製ステーを挟んでみました。

 が、ベースプレート自体、つい数日前に良さげなのを発見し、1個外して1個交換したものの、ほぼ意味をなしていなかった1本含め、100均のクランプ3本で仮固定してあったのですが、この様子だと、固定方法を変えただけでも電動については、現物合わせというか、微調整が必要になりそうな気がしなくもありません。

自作スマートカーテン外観

 仮固定とは言え、この1年間、最低でも1日に1度ずつ、手動で雑かつ豪快にカーテンを開・閉していましたが、微動だにしていないようです。

 数日前に1つ追加したクランプ以外プレート固定部分はいじってませんが、今回の電動テストにおいても動作・機能に全く影響ありません。

 よって正式に固定するにも代替案を思いつかないので、このままで完成にしておこうかな...とも。

 駆動部設置場所は、部屋のコーナーにあたる位置。

 壁2面ともコンクリート、塗装が微妙で例えば、ブルタックやテープなどを貼っても塗装がパズルのピースのように貼った部分が任意の形状に剥がれてしまう感じ。

 天井から吊るのも、下方の行き当たるところはデスク上のデッドスペースながら脚をつけるのも微妙。

 梁もあるにはありますが極薄なので。

 尚、サーボの固定もいろいろやってみたものの、今尚、仮固定状態ですが、結果行き着いたダイソーの200円商品クイックリリースクランプ 45mmが最も安定しているので、このままにしておこうかと。

 というわけで仮固定を正式固定とするということになれば、着手当時から頭の中ではできているリミットスイッチの取付方法を決め、マイコン周辺をまとめ、この部分にカーテンボックスを付ければ、完成です。

自作スマートカーテン駆動部

 内側はこんな感じで前掲写真の右奥側のグリーン部分のサーボホーンとユニバーサルバーをクランクに歯車+ステッピングモータ28BYJ-48ごとスライド・移動させ、カーテン駆動側歯車に着脱する構造になっています。

 ステッピングモータの台座には、厚さがちょうどよかったユニバーサル基板を、これを固定するためにバインダー(紙用バインダーの背表紙部分とほぼ同じもの)で挟みボルト止めしています。

 この構造の耐久性はテストもしくは常用して確認となるものの、計算苦手なので行きあたりばったりでイメージした通りに組み上げてみたら、タミヤの各種パーツのおかげもあり、奇跡的に躓くこともなく、あっさりできました。

 ちなみに前掲写真にあった紐は今となっては意味がないので取りました。

 第1案から第2案に変更したことでスケッチも一部、要変更。

 ふー、やっとできた、ただただ、ポカーンとしてなくてよかった。

[2021/05/19]

 動画もアップしてみました。

 駆動中モーター側が微妙にうねっているのは、きっと台座にしたユニバーサルボードに結構な反りがあるからかと。

自作スマートカーテン用マイクロスイッチ/リミットスイッチ

 リミットスイッチ(マイクロスイッチ)は開閉共用でこんな感じで取付決定。

 ラッキーなことにタミヤのユニバーサルプレート用スライダのパーツの1つが、横方向における取り付け位置調整の柔軟性もあって、このサイズのリミットスイッチを固定するのにちょうどよい感じでした。

 アングルパーツも脚として何かと重宝。

 あとは、チェーンに引っかからず、絶妙な長さでスイッチ先端にクロスになるようにゼムクリップか何か軽くて薄くて曲がらない程度に頑丈なものをホットボンド、なんなら念の為、極々少量のブル・タックで補強するなどして固定、チェーン上下それぞれ良き位置に付けた突起物がスイッチを押すようにします。

 もちろん、配線はあとでちゃんとハンダ付けします。

 というわけで、とりあえず、自動で電動・手動切り替えできるようになったのでここまでで、一通り機能するようになりました。

 残るはスケッチに開・閉ともに一定時間経過しても停止しない場合、途中でひっかかったなどの状況にあると判断し、停止するロジックを入れたいなー(入れました)という話とミニブレッドボードのままにするか、ハンダ付けし直してマイコンや配線部分をケーシング、追加分のカーテンボックスを作れば完成。

 っていうか、ESP32のタイマー、予想に反して超難解っぽいじゃん...、って、もしかして、そんなこと、できないの?

 ついでに金属など触れるだけでON/OFFできる静電容量タイプのスイッチ(Arduinoだとこんな感じ)を付けてカーテンの開・閉の途中で停止できるようにすることにしました。

 例えば、電動で開・閉している間に手動で開・閉したくなることもあるかなと(検証中につけておけば、高い位置にあるリミットスイッチを押しに行かなくても済むのもメリットかも)。

 これには、ESP32内蔵タッチセンサと割り込み(サンプルスケッチTouchInterrupt)を使うことにします。

 停止だけじゃなくて開閉スイッチもつけようかな、スマホやPCなくても操作できるし(原点回帰?)、あ、でも、開閉となるとタイマー自動開閉、パネル操作と物理的な手元操作パネル、何れかで動作中かどうかチェックもいるか、やめとこっかな。

クロスバー付き自作スマートカーテン用マイクロスイッチ/リミットスイッチ
[2020/05/23]

 雑ながら、リミットスイッチ先端にホットボンドでゼムクリップを付け、配線もハンダ付け、物理的な金属プレートの停止スイッチも仮付けながら付けました。

 扉を閉めた時はリミットスイッチが常に押された状態で...というのと違ってカーテンの場合、開閉後、それぞれリミットスイッチを押したら、その瞬間解放もしてくれないといけないので、さて。

 と思ったら、ラッキーなことにチェーンだからか開閉時共にサーボモーターが停止した際に反動でリミットスイッチも解放してくれたので偶然の産物に助けられました。

 が、何度も試してほぼ成功してはいるものの、常にそうなることを期待するというのは心もとないので一定時間経過で自動停止もなんとか組み込みたいところ...(組み込みました)。

 ところでカーテンを閉め切る時に負荷が、やや大きくなるようでリミッター位置を多少移動してみたところで必ずと言ってよいほど歯飛びし、数回、カタッカタッと音がしていました。

 当初は、幅200cmx高さ180cmは良いとして、このカーテン、意外なことに気づけば柄物カーテンと遮光遮熱カーテン2枚重ねで1枚ものになっており、他のに比べて結構重いからだよね、電圧もより下げられるし、やっぱり、軽快な動作実績のある他の一重の遮熱遮光カーテンに替えようかなと思っていました。

 まぁ、それはあるにしても、結果、モーター側ギアのシャフト含む構造においてモーターの反対側にあたるシャフト部が手前のモーター側に比べ、水平方向に押され、シャフトごと若干斜めになることが原因でした(その部分を軽く指で押さえてみたら歯飛びがなくなりました)。

 スライド溝はあるので上下方向はともかく、スライド溝方向の後方からの移動体の補強ってどうしたら...。

 スライド溝後方に追加で移動体を付けてもスムースに動いてくれないといけないので補強にはならない...。

 オモリをつけるとスライドに支障をきたす...。

 シャフトがしなっているわけではないのでシャフトを太くしても解決策にはならなさそう...。

 ベースプレート上に1点、固定しつつ、固定部を回転可能にした可変のバー...とすると弧を描いてしまう...、長穴にしてこれを回避すると補強にならなそう...。

 固定バーをモーター部共々スライドさせる構造に...、それが補強になるなら、そもそもスライドさせる追加構造物を作らず、補強したいところ...。

 モーター側の移動体と反対側のシャフトやカップリングをコの字型のバー状やプレート上のもので...と言っても、ちょうど良さげなものは思い浮かばない、作ればよいと言っても何でどう作ればよいものやら...。

 反対側も同じくスライダ&クランク機構に...、スライド溝の高さも違う上、行き当たりばったりで作ったモーター側と同一の軌道となる構造にするのが容易ではない?(もしかして計算能力があれば簡単?)

 この中なら、コの字型でいけるかどうか、妥当なのは、反対側もモーター側と連動するスライダ&クランク機構も...ぽやーーん。

 知らない内に勝手に改善されててくんないかなー、ゴロゴロー、ゴロゴロー(ずん飯尾さん風)。

 カーテンを替えるか、なんらかの対策をどうにかこうにか考えて講じられるのか、さて、どうしたものか...。

 カーテン替えるか。

 カーテンを替えてみました。

 が、だいぶ軽量ですが、何らかの障害があると歯飛びします。

 歯飛びはしていますが、28BYJ-48-5Vは、脱調はしていないので、トルク的には、まだ余力はあるのでこの程度のカーテンの重さの差は関係なかったようです。

 よって先のモーターシャフト延長線上の反対側が押し負けない程度に補強できれば、元の重いカーテンでもいけそうです。

 その補強がないの場合、開くことはできても、ほぼ閉まるものの、閉め切るところまでは厳しいかも。

 ちなみに、これにも起因するかと思いますが、手動と電動で開閉しきる位置が若干異なり、手動の方が、より多く開閉でき、開閉しきってしまうとリミッターがリミットスイッチを越えてしまい、リミットスイッチ先端の破損につながる為、手で開閉する場合、開閉しきらないように気をつける必要があります。

 赤外線LEDとフォトトランジスタレーザーとフォトレジスタなどをリミット用のセンサとする方法もあり、この場合、正常範囲から検知位置を越えても装置が壊れる心配はありません。

 が、リミットスイッチ同様、停止時に反動で非検知位置へうまく移動するか、調整可能かは未知数、また、検知位置を越えた場合、その位置から電動で戻した場合、リミットスイッチとは異なり、正常範囲に戻る際の通過でうまいこと検知して停止してしまうことを意識しておく必要があったりします。

 と思いましたが、電動と手動での開閉におけるリミットスイッチの件については、発想を変えて、停止時間を優先、手で開閉しきる位置付近にリミッターを配置することで、これを回避することにしました。

 とは言え、補強方法の方は、にわかには思いつきそうもないので、これの検討を頭の片隅に置いておきつつ、とりあえず、こうした条件をクリアした環境で運用するものとしておきます。

[2021/06/22]
 あ、違いましたね、やはり、ベースプレートの反り・歪みが大元の原因で、とてつもなく、うっかりしていましたが、よく見たら、片持のベースプレートにおいて支持がない側が明らかに下がってました。
 ステッピングモーター、サーボモータなど支持がない側に重みがよりかかっているので当然ですね...。
 そこで当て板を調整したところ、歯飛びもなくなりました。
 よって以降数行の思案は不要、心配は杞憂に終わりました。

歯飛び防止にシャフトの遊びを抑えるアングルを設置

[2021/07/18]
 と思ったら、再現したので、これに加え、なぜ、すぐに思いつかなかったのか、モータの奥側ではなく、多少なりとも摩擦抵抗はあるものの、回転を邪魔しない程度にチェーンスプロケット側の歯車が付いたシャフトをモータ側に押し込んで固定すべく、タミヤのアングルパーツをあてがう恰好でベースプレートに固定。
 これにより、歯飛びしていたタイミングなのか、一部、回転が鈍くなる部分がある気がしないこともありませんが、歯飛び自体はしなくなり、安定稼働するようになりました。

 ちなみに余計な負荷がかかっているから?そもそもそういうもの?なのか、あれ?歯車回らない...と思ったら、モータのカップリングが、まさかのユルユルに...、こんな風にカップリングのイモネジの緩みが出ることもあるようなので定期的にチェックするか、故障かな?と思ったら、その可能性があることも頭の隅っこに留めおいておくと良さ気。

 というわけで残るは、開閉中、一定時間以上経過で停止するプログラムの検討(完成しました)、マイコン類の格納、駆動部周辺のカーテンボックス。

前提

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

回路

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

電源

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

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";
 
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分間は、回転し続けてしまうので。

[2021/05/16]

 歯車着脱方法変更に伴い、スケッチの一部の修正を要します。

 サーボの向きにもよると思いますが、setup内のgearServo.write(forward);を削除、また、仲介させていた歯車が1つ減って回転が逆になるのでcurtain_open()内のcounterclockwise()/curtain_close()内のclockwise()を相互入れ替えすればいけます。

 また、先日、備考にも書きましたが、万一、途中でひっかかって、いつまでも開閉が完了しない場合、モーター過熱要因になり得るので開・閉ともに一定時間以上経過した場合、停止するロジックが欲しいですね。(組み込みました)

[2021/05/19]

 ESP32内蔵タッチセンサを使って静電容量式タッチ検出によるタッチパネルを仕様追加することにしたのですが、内蔵なので当該GPIOに配線し、ソフトウェア的にスケッチに追記するだけです。

ESP32開発ボード
ボードによって異なる!?
TouchGPIO
04
10
22
315
413
512
614
727
833
932

 ESP32開発ボードにおいて内蔵のタッチセンサとして割り当てられているGPIOピンは、Touch0〜Touch9と表記され、10個あるのでESP32開発ボードのピンレイアウトを参考に何れかを使います。

 全体像は、他も取り込んでからにする予定なので、今時点では箇条書きに留めます。

 サンプルスケッチTouchInterruptとESP32開発ボードのピンレイアウトを参考にしつつ、

  1. 宣言部にint threshold = 10;を追記
  2. 宣言部にbool touch_stop = false;を追記
  3. 関数void gotTouchStop(){ touch_stop = true }を追記
  4. curtain_open()/curtain_close()関数共にif (!digitalRead(LimitSW)if (!digitalRead(LimitSW) || touch_stop)に変更
  5. curtain_open()/curtain_close()関数共にgearServo.write(base);のあとにtouch_stop = false;を追記
  6. 念の為、setup内にgearServo.write(base);を追記
  7. setup内にtouchAttachInterrupt(T8, gotTouchStop, threshold);を追記
    第1引数T8は、ピンレイアウト上のTouch8、第2引数は検出時に呼び出す関数、第3引数は閾値

 尚、原因不明ながら、5の追記位置によっては、挙動がおかしくなったので注意、その位置なら不要なはずも6もその影響で念の為。

 とりあえず、ESP32開発ボードにピンヘッダがハンダ付け済みなら、当該GPIO(ここでは、T8=TOUCH8=GPIO33)にオスーメスのジャンパワイヤを挿してオスピンに指でつまむなど触れれば、タッチを検出してくれます。

 完成形としては、見栄えの良い金属プレートを見つけ次第、それに配線し、スイッチにする予定です。

 とりあえず、身近な金属プレートで試す中、threshold = 10;としましたが、この値は、環境によって変わるようです。

 自身の場合、GPIO33にジャンパワイヤ1本付ける分には、サンプルスケッチと同じ40でいけましたが、デスク上まで配線を延して金属プレートを付けた場合、40だとダメでタッチが効いた状態になり、if文にタッチの条件を入れているのでカーテンの開閉ができなくなりました。

 調べると、どうやら、閾値の値は、数値が大きいほど感度が敏感になる(スケッチ内のコメント部)とのことのようなので延長した分、鈍感にしてみようと最初に試してみたのが10で他は試していませんが、これでうまく機能した次第です。

 ただし、スケッチをアップロードする必要がある場合には、タッチピン用のジャンパワイヤをESP32ボードから外すか、付けるにしても1本に留めた方が良さげで、延長したままだと謎のエラーでスケッチのアップロードに失敗、なぜか、ジャンパワイヤ1本にしてみたら、いけました、後に延長したままでもいけたので接触不良があったようです。

 ちなみにESP32 Using the touch padsによるとTickerを使えば、タッチしていない状態や時間計測と併せてタッチする時間の違いで複数条件評価できるともあります。

[2021/05/25]

 電動開閉時、一定時間以上経過しても停止しない場合、ひっかかっている等、非常事態と判断、歯車及びモータへの負荷を極力避けるため、緊急停止するロジックできました。

 また、ひいては熱による火災などを避けるためには、正常に機能した際の時間と同じ、もしくはほんの僅かなプラスアルファくらいに留めるのが妥当でしょう。

 多少の負荷の違いもあるでしょうし、念の為、開く時、閉める時の値を別にすべく、それぞれ変数を用意しました。

  1. 宣言部にint unsigned int open_stop_time = 45;/unsigned int close_stop_time = 48;を追記
  2. curtain_open()/curtain_close()関数共、冒頭にclock_t startc, endc;/unsigned int idifftime = 0;/boolean emergencyflg = false;/startc = clock();を追記
  3. curtain_open()/curtain_close()関数共にwhile文冒頭のif文をif (!digitalRead(LimitSW) || touch_stop || emergencyflg)に変更、そのif文内にemergencyflg = false;を追記
  4. if〜else文の後にendc = clock();/idifftime = (endc - startc) / CLOCKS_PER_SEC;endc = clock();if (idifftime >= open_stop_time) { emergencyflg = true ; }を追記

 clock_tについては、http://linuxc.info/time/time2/を参考にさせて頂きました。

 Arduino IDEがC/C++ベースだけにC/C++のソースをそのまま使える(ものもある?)のは便利ですね。

/*
2021/05/25
ESP32版電動スマートカーテン
Wi-Fi操作/既定・可変タイマー付き/電動・手動自動切り替え
歯車x2タイプ:カーテン側歯車にモーター側歯車を着脱/スライダ&クランク
歯車が1つ減った為、curtain_open()/curtain_close()を相互入れ替え
ESP32内蔵静電容量式タッチセンサを使い、電動開閉途中で停止するロジック追加済み
開閉後一定時間経過で緊急停止ロジック追加済み
*/
#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>
#include <Servo.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 );
 
File fsUploadFile; // a File variable to temporarily store the received file
 
const char common_name[40] = "esp32_curtain";
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 = "";
// 自動タイマー既定時刻(1桁は先頭ゼロ埋め)
String default_open_hour = "7";
String default_open_minute = "30";
String default_close_hour = "18";
String default_close_minute = "0";
 
unsigned int open_stop_time = 45;
unsigned int close_stop_time = 48;
 
Servo gearServo;
int pos = 0;
const int forward = 180; // 正転角度
const int base = 120; // 基準角度
const int backward = 0; // 逆転角度
const int step_speed = 40; // 回転速度
 
int threshold = 10;
bool touch_stop = false;
 
#define JST 3600* 9
 
void gotTouchStop() {
touch_stop = true;
}
 
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");
 
clock_t startc, endc;
unsigned int idifftime = 0;
boolean emergencyflg = false;
 
startc = clock();
 
while (1) {
if (!digitalRead(LimitSW) || touch_stop || emergencyflg) {
setOutput(9);
emergencyflg = false;
delay(1000);
gearServo.write(base);
touch_stop = false;
break;
} else {
gearServo.write(forward);
}
endc = clock();
counterclockwise();
delayMicroseconds(motorSpeed);
delay(1);
idifftime = (endc - startc) / CLOCKS_PER_SEC;
if (idifftime >= open_stop_time) {
emergencyflg = true ;
}
}
}
 
void curtain_close() {
Serial.println("close");
 
clock_t startc, endc;
unsigned int idifftime = 0;
boolean emergencyflg = false;
 
startc = clock();
 
while (1) {
if (!digitalRead(LimitSW) || touch_stop || emergencyflg) {
setOutput(9);
emergencyflg = false;
delay(1000);
gearServo.write(base);
touch_stop = false;
break;
} else {
gearServo.write(forward);
}
endc = clock();
clockwise();
delayMicroseconds(motorSpeed);
delay(1);
idifftime = (endc - startc) / CLOCKS_PER_SEC;
if (idifftime >= close_stop_time) {
emergencyflg = true ;
}
}
}
 
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);
 
// irhour = atoi(rhour);
 
sprintf(rminute, "%02d", tm->tm_min);
delay(1000 - millis() % 1000);
 
// irminute = atoi(rminute);
 
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) {
Serial.println("Auto Open");
curtain_open();
}
// 既定または、指定時刻に閉める
if (String(rhour).equals(set_close_hour)
&& String(rminute).equals(set_close_minute)
&& irsec <= 6) {
Serial.println("Auto Close");
curtain_close();
}
}
 
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);
gearServo.write(base);
}
else if (server.arg(0).indexOf("CLOSE") != -1) {
Serial.println("CLOSE");
curtain_close();
setOutput(9);
gearServo.write(base);
}
else if (server.arg(0).indexOf("SET") != -1) {
Serial.println("server.arg(0) *** ");
Serial.println(server.arg(0));
Serial.println();
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);
}
Serial.print("opentime opentime || closetime ");
Serial.print(opentime);
Serial.print(" || ");
Serial.println(closetime);
Serial.println();
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;
}
Serial.print("set_open_hour:set_open_minute ");
Serial.print(set_open_hour);
Serial.print(" : ");
Serial.println(set_open_minute);
Serial.println();
 
set_close_hour = set_close_minute = "";
if (close_hour != "") {
set_close_hour = close_hour;
set_close_minute = close_minute;
}
Serial.print("set_close_hour:set_close_minute ");
Serial.print(set_close_hour);
Serial.print(" : ");
Serial.println(set_close_minute);
Serial.println();
 
return true;
}
 
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) {
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!!");
}
}
 
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", "");
});
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 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);
gearServo.attach(25);
// gearServo.write(forward);
gearServo.write(base);
touchAttachInterrupt(T8, gotTouchStop, threshold);
 
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
// MDNS.update();
autotimer();
}

 というわけで電動開閉中に手動操作したくなった場合などに使える金属プレート停止用操作パネルと共にソース全体はこんな感じになりました。

 先頭のタブとなるスペースは端折りました。

 autotimer()のカーテンを閉じる時の時間比較における追加評価の秒を3から6に変更しました(デフォルトのタイマー設定で、なぜか閉じる時だけ、空振ることがあったため)。

 ちなみにオートタイマーと緊急停止ロジックで共有できればと思い、autotimer()から分離してポインタ渡しで時間取得関数を作ってみました。

 が、前者では機能したものの、後者では時間がかかり過ぎるようでステッピングモータが回らかったので、今回の方法に落ち着き、兼用できなければ意味もないので元に戻しました。

...
const int restartLED = 17;
...
setup() {
...
  pinMode(restartLED, OUTPUT);
...
}
...
void handleroot() {
...
  else if(server.arg(0).indexOf("RESTART") != -1) {
    Serial.println("RESTART");
    delay(1000);
    digitalWrite(restartLED, HIGH);
    delay(1000);
    digitalWrite(restartLED, LOW);
    delay(2000);
    ESP.restart();
  }
...
}
[2021/08/13]

 なんらかの入力エラーのようで操作後、たまにステッピングモータドライバULN2003の入力信号LEDがランダムに消灯、ESP32ボードをハードウェアリセットしないと機能しない現象に遭遇していたので、重い腰を上げて超簡単なESP32のソフトウェアリセットESP.restart()を入れることにしました。

 Arduino IDEにおいては、ESP32には、ESP.restart()が、ESP8266には、ESP.restartとESP.reset()が、esp-idfでは、esp_restart()があるようです。

 本番では要りませんが、一応、このロジック通ったよ確認用LEDも残しておくとこんな感じ。

 操作パネルからリセットできるようにHTMLファイルにRESTARTリセットボタンを追加。

[2021/09/28]

 ぬぬ...ソフトウェアリセットじゃダメでした...。

 あ、digitalWriteすればよいのか...。

 エラーが起きてからだとESP32にアクセスできないから、パネル開いている時、限定になっちゃうけど、いっか...。

 ありゃ、それ以前にエラー時にピンにアクセスできないっぽく?これもダメか...。

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はよしなに。

 尚、なぜか、パネルを開いてから30秒ほど時間をおかないとボタンを押しても機能しない、即押して、あれ?動かないと思って、また押すとダブって複数回実行されてしまうことがあるのでパネルが開いてから概ね30秒後に操作するのが無難そうです。

 もちろん、電動で開け切る時、閉め切る時は、リミットスイッチが効くようにしておけば、仮に操作が重複したとしても問題ありませんし、静電容量式の停止用タッチパネルで停止させることもできますが。

<body>
...
<center><div style="padding:6% ;width:36% ;"><input id="clear" type="submit" onclick="sendCtrl('RESTART');" value="リセット" style="width:120px ;height:40px ;"></div></center>
...
</body></html>
[2021/08/13]

 ESP.restart()によるソフトウェアリセット機能を追加するに伴い、onclickで文字列RESTARTを送信するリセットボタンを追加。

操作方法

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越しの無線操作に加え、自作ラズパイスマートスピーカーから電動部自作カーテンを音声操作することも簡単にできます。

2020/04/27

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

2020/12/18

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

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

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

備考

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

2021/04/25

 そう言えば、ひっかかって途中で止まってしまった場合を考慮して例えば、一定時間内に開、または、閉しなかったら止めるっていう機能も必要ですね(組み込みました)。

 あとスリープも欲しいところですが、この仕組みにおいて妥当な復帰方法がなさ気なので、なしで。

ウェブ造ホーム前へ次へ