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

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

ホーム前へ次へ
ESP8266って?

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

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

2020/03/07

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

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

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

2023/03/06

 以前のバージョンは電動・手動が排他でしたが、何れでも操作できるようにしてみました。

 つまり、紐付きのPanasonic WG4484PK(引掛シーリング増改アダプタ4型)はお役御免。

 以前というか昨日まで電源直のWG4484PKを使用していた時は、ON/OFFのみだったのでON時は、過去、2灯/1灯/常夜灯/OFFとペンダントライト自体の紐を引いた時の状態に依存していましたが、遠隔操作でも紐を引く度、2灯/1灯/常夜灯/OFFと切り替わるようになりました(どっちが良いのか微妙?)。

 となると同じのを使いまわしてますが、操作パネルも変えた方がいいかな...。

自作スマートペンダントライト常夜灯付近に配置することにしたサーボ

 結果、サーボの位置は、その記憶にあるページの通り、常夜灯の横にスイング代調整の為、以前も使っていた嵩増しの木片+サーボをナノテープで固定し、配置(サーボが紐を重力方向に引くと木片+サーボがナノテープ固定面を押し上げる恰好となるので接着が強化される...気がする)。

 サーボは、ペンダントライトのサークライン(円形の蛍光灯をそう呼ぶらしい)の蛍光灯をはめてあるアームを全て外すことで上面と下面に分解し、それぞれ良き位置に手持ちのドリル最も太いもので6.5mmだったこともあり、できればつながるほど近くにドリルで穴を2つあけ、それぞれダイヤモンドヤスリでバリ取りしつつ穴を1つにし、そこを通して常夜灯横のサーボの線(GND/VCC/DATA)をサークライン構造体の上面に配線。

 ペンダントライトの紐は、サークライン下面までスイッチに繋がれた輪っか状の紐が出た状態で、そこに縛られていたので、その輪とサーボホーンの先端の穴をちょうどピッタリだった100均のゼムクリップでつなぎました。

 スケッチも一切変更なしで動作確認してみると紐は、手でも引けるし、スマホからも引けるということで成功。

自作スマートペンダントライトの電源と回路ケース(改)

 以前は、装置や配線類のカバーとしてちょうど天井くらいまで乳白色ボトルを使っていましたが、今回は、100均のカードケースに変更、これにESP32とブレッドボード小を収納、サークライン構造物上面とカードケース底面をナノテープで固定。

 尚、以前、5VのACアダプタをサーボ用電源としていましたが、いくつか検証中の装置類では不要なのに...と思ったら、使用していたダイソーに以前あった200円商品のキューブ型USB充電器が不調だったようで(というか、自分でも怪しいぞとマーキングしていた)、ダイソー200円商品のスリム型USB充電器に替えてみたところ、USB電源だけでいけたのでACアダプタは不要となりました。

 コンセントソケット付きPanasonic WG4481PK(引掛シーリング増改アダプタ1型)にスリム型USB充電器を直だとUSBケーブルが嵩張る、USBケーブルは下方向に出したい、が、コンセントの向きを変えることができる先端に丸みのあるダイソーの縦型L字電源プラグだと天井にあたり気味で微妙、そこでスイングタップ(平たい3個口)の下方向にスリム型USB充電器を、そこにUSBケーブルを接続することに。

 また、余計なスイッチ付きコンセントも不要となり、すっきり。

自作スマートプルスイッチ2号機かつ2台めのペンダントライト
2023/03/31

 今日、もう1部屋分、手動・電動ともに使えるスマートプルスイッチを作って同じ方法でペンダントライトに設置・運用開始。

 ただ、今回は、ESP32ボードとサーボはブレッドボードもユニバーサルボードも介さず、ジャンパワイヤで直結。

 また、長すぎる配線は再結束できるシリコン製結束バンドで緩く結束。

 ESP32ボードについては、さすがにホコリ対策は必要かと配線部分を切り欠いたナノテープで固定のケースには収納。

 ドリルによる穴あけは、サークライン下面のみ、天板には、良き隙間があったので、そこを通すことに。

 尚、台数制限を超えたわけではなく、距離的なものと思われますが、集中させていた無線LANルータ(アクセスポイント)では微妙に届かないっぽく、近くでは完璧に動くも、運用場所だと明らかに挙動不審で動作不安定につき、より近い無線LANルータに接続、解決。

2024/10/01

 この緑っぽい傘の電気の紐が切れたので分解、元を見るとスイングする穴に紐がたまむすびになっているものの、込み入っていてやりづらそう...。

 まぁ紐買って無理なら、ついでにLEDシーリングライトに...と探すとヨドバシでも最安57円と激安...とは言え、紐はしばりにくいしな...と思ったら、紐よりは取り付けやすそうな200円もしないボールチェーンの金属製を発見...。

 が、いや待てよ、それにしても狭くて付けられないかもしれないということもさることながら、NEMA17モーターに使うのもありかとボールチェーンボールチェーン用コネクタって前に買ったよね...。

 ということは、対処できれば、配達を待つまでもなく、すぐに照明も使えるわけで、紐は必要なら買うにしてもスマートペンダントライトとしてはなくても、ね?

 と、やってみたら、あっさりできました。

 照明を設置、スイングスイッチに通したボールチェーンを垂らした状態で手でチェーンコネクタに引っ掛けるスペース上の制約もあり、ちょっと長めにした分、サーボホーンとのつなぎに使っているゼムクリップの長さや曲げを調整する必要はありましたが。

自作スマートプルスイッチ3号機にして1台めのシーリングライト
2023/04/01

 今日、更にもう1部屋分、スマートプルスイッチを作って今度は、シーリングライトの天板上に設置・運用開始。

 よってドリルによる穴あけは、なし。

 シーリングライトに付属の紐を手で引いても、サーボでサーボ用の紐を引いても他方は紐が弛む方向なので手動でも電動でも操作できる点は、ペンダントライト用の自作プルスイッチ1号機、2号機同様。

 長すぎるサーボ配線は再結束できるシリコン製結束バンドで緩く結束。

 2号機と同じく、より近い無線LANルータに接続。

自作スマートプルスイッチ+シーリングライトのESP32/MG90S/スペーサーという名の100均プラ製どんぶり
天井から外した状態のシーリングライト

 天井と隙間がほぼないシーリングライトだけにコンセント付き及びシーリングアダプタ装着時のスペース確保(1号機で不要となった紐付きPanasonic WG4484PKを流用)と引掛けシーリング2つ連結したこともあり、スペーサーが必要。

 ということは想定していたので以前、キャンドゥで3個100円のプラのどんぶり 白(写真中央、ESP32が顔を出せるよう切り欠いてあるもの)を購入済み。

 まさか、こんなところに、どんぶりが潜んでいるとは、誰も気づかないはず。

 円周上にある黒いのは元から付いていたスポンジのスペーサー。

 つまり、引掛けシーリング2個でこれだけ(どんぶりと黒いスポンジの高さの差ほど)の空間ができてしまうということ。

 さておき、電動のときは良好も手で引くときは、紐が中央でなく、シーリングライトカバーより外の端にある(写真左下方向にあるプレートがカバー外まであって紐がぶら下がる恰好になる)こともあって、わずかに不安定さが残るので天井とシーリングライト天板の間の高さ分、できるだけ外側に数箇所、もしくは紐のある対角に1箇所、超軽量なスポンジとかスチロール(レンガ)とかでも挟んだ方が良さ気。

 サーボは、最終的にペンダントライト1号機、2号機で取り付けた位置とは異なり、天板上のちょうどスイッチの上あたりに配置、結果、スイッチをくの字に引く恰好となり、スイング動作すると引き剥がされる方向に力が働くのでどうかと思ったものの、ナノテープで固定。

 もし、剥がれるようならサークラインに2箇所穴あけし、サーボを直で結束バンドで固定、もしくは、プラバンなり、アルミ板なりを几の字にしてサーボを挟み、天板に穴あけの上、(ビスとナットだと落下すると危ない、標準の既存ビス同様、サークライン上に穴あけ及び隆起させタップを切るの難しいというか、面倒なので)結束バンドで共締めすれば良いかなと。

 サーボホーンに引くほど締まる方法で紐を縛り、シーリングライト付属の紐が縛ってあるシーリングライト下面側にあるスイッチから出た閉じた?状の針金に同様に縛りました。

 ペンダントライト版1・2号機はサーボのスイングは160度ですが、シーリングライト版は引ききれなかった為、180度としました。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2020/03/24

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

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

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

前置き

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

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

使ったもの

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

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

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

前提

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

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

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

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

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

回路

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

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

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

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

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

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

スケッチ

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

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

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

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

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

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

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

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

2023/03/31

 Arduino IDEが賢くなった点含め、今日時点、エラー回避のため、以下、追記が必要となっていました。

HTML/JavaScript

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

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

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

操作方法

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

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

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

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

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

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

スクリプトから操作

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

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

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

2020/04/27

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

2020/12/18

 しばらく使わずにいたら、pythonのwebsocket周りの仕様が変わったらしく、ハマりましたが、websocketとwebsocket_clientの競合でうまくimportできないのを解決するに救われました。

 使えていたはずのスクリプトが、どうにも機能しない...何度も書き換えてみたけどダメ、python3系のアップデートかと思ってやってみてもダメ、pip3でwebsocketをアップデートする必要があるのか?とアップデート。

 その後、前掲の自作スクリプトを実行してみたら、今までにないエラー[ImportError: cannot import name 'create_connection' from 'websocket']に遭遇、これをキーに検索した結果、リンク先にたどり着き、websocketではなく、websocket-clientを使わなければならないことを知った次第。

 ただ、リンク先でもwebsocketのuninstallとwebsocket-clientのinstallの順に言及しており、これならいけたはず...という順でも、結局、websocket-clientを一度、uninstallしてから再installしないと自作スクリプトが機能しないという謎は残りましたが、元の通り、機能するようになりました。

 この変な挙動気になる...。

 というか、自身の場合、websocketというか、websocket-client-py3を入れてあったんだった...これもダメでwebsocket-clientにする必要があったってことですよね...、アンインストールしていないので今尚、websocket-client-py3も入っていますが、支障はないようです。

 ちなみに、このスクリプトは、自作スマートスピーカーにおける音声操作に際して使っており、ブラウザ版スマートホーム操作パネルでは、JavaScriptのwebsocketであり、ブラウザから操作する際には、影響ありませんでした。

 それにしても、こういう仕様変更は、使いどころによっては、影響が大きすぎると思うので避けた方がよいと思いますが...やるなら、せめて、大々的に告知するとか...難しいだろうけど、人気があるからって、あまり無茶すると離脱者が続出しかねない...。

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

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

ホーム前へ次へ