以前からカーテンの自動化には興味があったが探してみると横に引くタイプの物は数多く出回っているが日本で圧倒的なシェアを誇るというか我が家にたまたま付いてたTOSOクリエティドラム・ツインワンチェーンに対応したものはない。なぜないのかはわからないがないなら作るまでということで頑張って作ってみることにしてみた。
ワンチェーン・タイプはチェーンを引く操作だけで外側と内側の2つのカーテンを上げ下げすることができるすぐれものであるが制御するのはそう難しくもない。だが設定時刻のみでカーテンを上げ下げするだけではなんとなくつまらないし、我が家ではルンバが活躍してくれているのでルンバの掃除を邪魔しないようカーテンを少し持ち上げる機能もほしかったりする。また、私だけかもしれないが長年生きていると外が明るくなったら起きて暗くなったら寝るという生活リズムが体に染みついてくるようなので2個の光センサーによる室内外の明るさや経緯度から自動計算した日の出/日の入り時刻により開閉できるようにしてみた。経緯度はグーグルマップで調べることが出来るしタイムゾーンオフセットと組み合わせることで地球上の全地域の日の出と日の入り時刻が計算可能となる。但し、近似式による計算であることに加え標高なども考慮してないのでそれなりの誤差(数分程度)は出てしまうが精度はそれほど重要でないので問題ないはずだ。
日の出と日の入り時刻の計算についてはラジオペンチさんの記事を参考にさせて頂いた。ラジオペンチさんはHW/SWともに秀でている優秀な方のようだ。感謝です。<(_ _)>
Arduinoで日の出・日の入り時刻を計算
※うるう年とUTC対応を行った。
今回のCPUは在庫処分したかったESP8266(WROOM02)と最近投稿したATtiny10のLED光センサーを組み合わせてみた。なお、光センサーによる制御は夜間の自動車のライトなどの外光に反応し開閉することがないよう昼間の時間帯(設定可能)のみの制御としている。ちなみにファームウェアのESP32対応はADC処理のみの簡単な変更でOKなのでなんでいまどきESP8266使うのって思う人はぜひ挑戦してみて頂きたい。
カーテン開閉用のモーターは、アマゾンで見つけた Bringsmart 小型 dcモーター 電動機 12v 30rpmの軸径と軸長がちょうど良く消費電流も少なくていい感じだ。トルクと開閉速度との兼ね合いが見当もつかなかったので66rpmと90rpmのものも試してみたがトルクが足りず30rpm以下のものででないと駄目だという結論に落ち着いた。カーテン開閉時間は2分半程度と遅いが回転が早すぎたりトルクがありすぎたりするとカーテンを壊してしまう恐れもあるし普段見ていないところで勝手に開閉するのであれば多少遅くとも気にはならないだろう。ちなみに、このモーターの気になる点は少し煩いところと耐久性。耐久性は実際に使ってみないとわからないけどね...
モーター制御にはブラシ付きDCブラシモータドライバ TB67H450FNGを使ってみた。使いやすくて便利なチップだ。
カーテン制御はカーテン上昇端の判定をカーテンの保護も兼ねてモーター電流の増加で判断しているがカーテン開閉に必要なトルクを得るための電流値はカーテンの重量等にも影響されるため最大電流値は設定により変更可能としている。また、起動直後のカーテン開閉状況がわからないことから起動時にはカーテンを一旦閉めてしまうことで開閉状況を合わせるようにしているが開閉時のモーター駆動時間や最大電流値の設定が不適切だと開閉状態が合わなくなってしまうことがある。そういう場合は設定値を調整する必要があるが設定値の範囲はそんなにシビアなものではないので調整は簡単に出来るはずだ。
カーテン制御は以下のように行われる。カーテンの状態にかかわらず確実に開閉させるためには余計なステップが必要となる。
【カーテンを開ける場合のモーター制御】
1.モーターを確実に回すために少しだけ逆回転させる。(時間※)
2.モーター電流が設定値を超えるまで正回転させる。
3.カーテン負荷を抜くために少しだけ逆回転させる。(時間1)
【カーテンを閉める場合のモーター制御】
1.モーターを確実に回すために少しだけ逆回転させる。(時間※)
2.カーテンが確実に持ち上がるまで正回転させる。(時間2)
3.カーテン負荷を抜くために少しだけ逆回転させる。(時間1)
4.カーテンが落ちる(かもしれない)まで待つ。(時間3)
5.カーテンを少し持ち上げるために正回転させる。(時間1+α)
6.カーテンを落とすために逆回転させる。(時間1+α)
7.カーテンが落ちるまで待つ。(時間3)
8.ルンバ用にカーテンを少し持ち上げる。(時間4)
※過負荷によりモーターが回らないときの電流値は最大電流未満かつ一定となることに注意しよう。
モーターは回れ!と命令しても物理的に回らないときがあるのが厄介かもしれない。そういえば、昔昔、プログラムは正しく命令しているのだから回らないモーターのほうが悪いと訴える若い開発者(もしかしてそういう私も若かった?)を見かけたことがあるが大きな間違いだ。スイッチを入れれば絶対回るという考え方に問題がある。そういう気づきができるかどうかが将来の分かれ道になると思ったほうがいいだろう。経験しないとわからないことかもしれないが...
【モーター取付ブラケット】
現物合わせで作ったブラケットとプーリー。3Dプリンターは便利だ!
【取付手順】
+ネジを外す。外したネジは再利用するので無くさないようにしよう。
プーリーを外す。レール穴径は6mm。6mm以上の軸径だとレール穴には入らないのでアダプターの厚み調整が必要となる。
プーリーとチェーンはどっかにしまい込むと間違いなく忘れて行方不明になるのでネジ止めにかけておくのがお勧めだ。
本体ケースをセットした取付ブラケットを取り付けて完了。取り付けには1分もかからないし元に戻すのも簡単だ。
両面テープで固定した手動開閉SW。上ボタンは内側カーテン、下ボタンは外側カーテンに対応。短く押すとカーテンが開き、3秒以上の長押しでカーテンが閉まる。同時押しで停止。同時押し+長押しで両側カーテンを閉める。などの操作が出来る。開閉はMQTT経由でも可能。
両面テープで固定した外光センサー。
ルンバ用にカーテンを持ち上げておくことができる。
只今、絶賛量産中!(笑)
【回路図】
抵抗は少ないけどコンデンサーがこれでもかっていうくらいある。省略可能なコンデンサーもいくつかあるんだけどハンダホールを見てしまうと無性にハンダ付けしたくなるのはなぜだろう?(笑)
【基板】
上に飛び出してるのは室内光センサー。というとなんだかカッコいいけど中華製の安物のLEDでっせ。でも感度いいアルよ!(笑)
ケースの外側に露出させるため飛び出させているが上にエアコンがあったりすると室内照明の影になってしまうため外付けセンサーとしたほうが良かったかも...いまさらですけど。
【プロパティ・ページ】
こんな感じの設定にすると、日の出以降の外がある程度明るくなってきたときにカーテンを開け、日の入り時刻になったとき或いは外が暗くなり室内照明を付けたときにカーテンを閉めるという動きをさせることが出来る。
室内外の光センサーのスレッショルド値は、センサー値が設定値より低ければ明るいと判断し高ければ暗いと判断する。設定のおおよその目安であるが、室内光センサーは夜に照明を付けたときのセンサー値よりも少し大きく(+2000くらい)し、外光センサーはたまには早起きしてみてこのぐらいの明るさならカーテン明けてもいいかなというときのセンサー値を設定すれば良いだろう。現在のセンサー値はスレッショルド値の横の括弧内に表示しているので参考にしよう。ちなみにセンサー値10000は照明を必要とする暗さだ。
【アレクサ等との連携】
Alexaと連携できるライブラリで有名なのはesp8266-alexa-wemo-emulator/fauxmoESP/espalexaあたりのようであるがどれも使い方に制約があるし、そもそもこれらは既製品のフリをするエミュレーションなので突然使えなくなってしまう可能性大だ。で、他の物をと探してみたところESP8266にも対応しているSinricProを見つけた。
これはAlexa/GoogleHome/IFTTT(予定)に対応し3デバイスまで無料で使えるがそれ以上は1デバイスあたり年間3$の追加費用がかかる。費用的には問題ない料金ではあるがデバイス登録が増えるとセットアップやメンテナンスが大変になるだけだし家に3個以上のカーテンは普通にあるのではないだろうかということで無料で使えるデバイス1個だけ登録して複数のカーテンを制御する方法を思いついた。ブラインド・デバイスは電源オンオフと開閉状態(0-100)が制御できるから状態としては電源2個+開閉101個で計103個の状態が制御できる。それを分散割り当てすれば複数デバイスが制御できることになる。そういう使い方が良いのかどうかは微妙な気もしないではないが制御情報をどう使うかまでは規定されていないようなので問題はないと思う。たぶん。早速作って見たのはSinricProからの制御情報をMQTT制御メッセージに変換して送信するだけのアプリだ。最大9個までのカーテン制御が可能となる。カーテンの開閉状態(0-100)を10で割った値をカーテンIDとし、10で割った余りの値でカーテンの開閉状態を指定するものとする。ちなみにカーテンID0は全カーテンを制御する。
アレクサ連携は便利だ。声で指示するだけでカーテンの開け閉めができるなんて楽過ぎてどんどん堕落の底に落ちてしまいそうだ。(笑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
0 ... all curtains outer 100% (closed) 1 ... all curtains outer 75% (not supported) 2 ... all curtains outer 50& (not supported) 3 ... all curtains outer 25% (not supported) 4 ... all curtains outer 0% (opened) 5 ... all curtains inner 100% (closed) 6 ... all curtains inner 75% (not supported) 7 ... all curtains inner 50& (not supported) 8 ... all curtains inner 25% (not supported) 9 ... all curtains inner 0% (opened) 10-19 ... curtain 1 (outer close = 10, outer open = 14, inner close = 15, inner open = 19) 20-29 ... curtain 2 (outer close = 20, outer open = 24, inner close = 25, inner open = 29) 30-39 ... curtain 3 (outer close = 30, outer open = 34, inner close = 35, inner open = 39) 40-49 ... curtain 4 (outer close = 40, outer open = 44, inner close = 45, inner open = 49) 50-59 ... curtain 5 (outer close = 50, outer open = 54, inner close = 55, inner open = 59) ... 90-99 ... curtain 9 (outer close = 90, outer open = 94, inner close = 95, inner open = 99) 100 ... stop all curtains |
当初はカーテン・ファームウェアに組み込むつもりであったがメモリ不足で動作しなかったためESP8266/ESP32で動作する単体アブリとして作ってみた。余っているESP8266に書き込んでどっかに転がして置けばいいだろう。但し、ESP8266だとかなり厳しいメモリ状況で動作するため誤動作の心配がある。ESP32或いはRaspberryPiを使ったほうが精神的には良さそうだ。MQTTサーバー(mosquitto推奨)がない場合はRaspberryPiのほうが良いかも。
【SinricProコネクト(ESP8266/ESP32)】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
/* esp8266_sinricpro.h - SinricPro Connect for esp8266/esp32 Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include <stdint.h> #include <stdbool.h> #include "espwnet.h" #include "SinricPro.h" #include "SinricProBlinds.h" #define SINRIC_CONFIGURE_FILE "sinric.conf" // // MQTT spec // // N: Curtain ID (1,2,3,...) // // [topic] [payload] // /home/curtain/control/inner/N "UP" or "DOWN" or "STOP" // /home/curtain/control/outer/N "UP" or "DOWN" or "STOP" // static const String MQTT_CONTROL = "/home/curtain/control/"; static const String MQTT_INNER = "inner/"; static const String MQTT_OUTER = "outer/"; static ESPConf conf; static int sinric_status; bool onPowerState(const String &deviceId, bool &state) { String cmd = state ? "ON" : "OFF"; ESPWNet.publish((MQTT_CONTROL + MQTT_OUTER + "0").c_str(), (uint8_t *)cmd.c_str(), cmd.length()); ESPWNet.publish((MQTT_CONTROL + MQTT_INNER + "0").c_str(), (uint8_t *)cmd.c_str(), cmd.length()); return true; } bool onRangeValue(const String &deviceId, int &position) { (void)deviceId; String cmd; if (position < 100) { int id = position / 10; int inout = position % 10; cmd += 100 - (inout % 5) * 25; ESPWNet.publish((MQTT_CONTROL + (inout >= 5 ? MQTT_INNER : MQTT_OUTER) + id).c_str(), (uint8_t *)cmd.c_str(), cmd.length()); } else { cmd = "STOP"; ESPWNet.publish((MQTT_CONTROL + MQTT_OUTER + "0").c_str(), (uint8_t *)cmd.c_str(), cmd.length()); ESPWNet.publish((MQTT_CONTROL + MQTT_INNER + "0").c_str(), (uint8_t *)cmd.c_str(), cmd.length()); } return true; } void setupSinricPro() { String device_id = conf.getProperty("sinric", "device_id" ); String app_key = conf.getProperty("sinric", "app_key" ); String app_secret = conf.getProperty("sinric", "app_secret"); ++sinric_status; if (device_id.length() && app_key.length() && app_secret.length()) { ++sinric_status; // get a new Blinds device from SinricPro SinricProBlinds &myBlinds = SinricPro[device_id]; myBlinds.onPowerState(onPowerState); myBlinds.onRangeValue(onRangeValue); // setup SinricPro SinricPro.onConnected([](){ Serial.printf("Connected to SinricPro\r\n"); }); SinricPro.onDisconnected([](){ Serial.printf("Disconnected from SinricPro\r\n"); }); SinricPro.begin(app_key, app_secret); } } static void onSinricProPage(void) { String html; // // Save Properties and Restart // if (ESPWeb.arg("apply").equals("apply")) { conf.clear(); conf.setProperty("sinric", "device_id" , ESPWeb.arg(F("sinric_device_id" )).c_str()); conf.setProperty("sinric", "app_key" , ESPWeb.arg(F("sinric_app_key" )).c_str()); conf.setProperty("sinric", "app_secret", ESPWeb.arg(F("sinric_app_secret")).c_str()); conf.save(SINRIC_CONFIGURE_FILE); ESPWNet.onRestartPage("/sinricpro"); } // // Edit Properties // html = F("<html lang='en'><head><meta http-equiv='content-type' content='text/html; charset=utf-8'>"); html += F("<meta http-equiv='content-style-type' content='text/css'><style type='text/css'><!--"); html += F("table{border-collapse: collapse}th{background-color: #cccccc; border: solid thin #FFFFFF; padding: 2pt; width: 10em; text-align: left;}"); html += F("td{background-color: #eeeeee; border: solid thin #FFFFFF; padding: 2pt;}--></style><title>Curtain</title></head>"); html += F("<body><form action='/sinricpro' method='post'><table><tbody>"); html += F("<tr><th>Device ID</th><td><input type='text' style='width:20em' name='sinric_device_id' value='"); html += conf.getProperty("sinric", "device_id"); html += F("')</td></tr><tr><th>App Key</th><td><input type='text' style='width:20em' name='sinric_app_key' value='"); html += conf.getProperty("sinric", "app_key"); html += F("')</td></tr><tr><th>App Secret</th><td><input type='text' style='width:40em' name='sinric_app_secret' value='"); html += conf.getProperty("sinric", "app_secret"); html += F("')</td></tr></tbody></table><p><input type='submit' name='apply' value='apply'></p></form></body></html>"); ESPWeb.send(ESPWNET_HTTP_STATUS_OK, F(ESPWNET_HTML_CONTENT_TYPE), html); } void setup() { Serial.begin(115200); ESPWeb.on("/sinricpro", [](){onSinricProPage();}); ESPWNet.addHtmlRootLink("/sinricpro", "SinricPro"); ESPWNet.begin(); conf.load(SINRIC_CONFIGURE_FILE); } void loop() { if (ESPWNet.handle()) { switch (sinric_status) { case 0: setupSinricPro(); break; case 2: SinricPro.handle(); break; } } } |
【SinricProコネクト(RaspberryPi with python3)】
Pythonって多く人たちがライブラリを作ってくれたおかげで凄く便利に使えるというのは理解できているのだがなんとなく好きにはなれないし覚える気にもなれないのはなぜだろう...生きてきた時代のプログラミングに対する思いの違いなのかなぁ...
それはともかく、このプログラム動かすとCPU100%になるのはちょっと頂けない。なんとかしてほしいなぁ...と思って調べてみたら、最後の client.handle_all(udp_client)をclient.handle_all(udp_client, sleep=1)のようにスリープ引数に1を指定すれば良いみたいだ。但し、ループ中に単純にスリープさせるだけみたいなので最大1秒の遅延が発生するものと思われる。カーテン制御なので1秒程度の遅延は問題ないのだが完全なイベント駆動型にしたいところだ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
from sinric import SinricPro from sinric import SinricProUdp from credentials import appKey, blindsId, secretKey, deviceIdArr from time import time, sleep import os CURTAIN = '/home/curtain/control/' PUBLISH = 'mosquitto_pub' RESTORE = 15 def onPowerState(did, state): # Alexa, turn ON/OFF Device global start if start != 0: start = time() if time() - start < RESTORE else 0 # print(int(time() - start if start != 0 else -1), ': onPowerState(', did, ',', state, ')') if start == 0: cmd = ('ON' if state != 0 else 'OFF') os.system(PUBLISH + ' -t ' + CURTAIN + 'outer/0' + ' -m ' + cmd) os.system(PUBLISH + ' -t ' + CURTAIN + 'inner/0' + ' -m ' + cmd) return True, state def onBlindsAction(did,state): global start if start != 0: start = time() if time() - start < RESTORE else 0 # print(int(time() - start if start != 0 else -1), ': onBlindsAction(', did, ',', state, ')') if start == 0: if state < 100: id = str(state // 10) level = state % 10 cmd = str(100 - (level % 5) * 25) topic = CURTAIN + ('inner/' if level >= 5 else 'outer/') + id os.system(PUBLISH + ' -t ' + topic + ' -m ' + cmd) else: os.system(PUBLISH + ' -t ' + CURTAIN + 'outer/0' + ' -m STOP') os.system(PUBLISH + ' -t ' + CURTAIN + 'inner/0' + ' -m STOP') return True, state callbacks = { 202 'powerState': onPowerState, 'setRangeValue': onBlindsAction } if __name__ == '__main__': start = time() client = SinricPro(appKey, deviceIdArr, callbacks, enable_log=False,restore_states=True,secretKey=secretKey) udp_client = SinricProUdp(callbacks,deviceIdArr,enable_trace=False) # Set it to True to start logging request Offline Request/Response client.handle_all(udp_client, sleep=1) |
1 2 3 4 5 6 7 |
appKey = '' # <--ここにsinricproに登録したときのAPP_KEYを設定 secretKey = '' # <--ここにsinricproに登録したときのAPP_SECRETを設定 blindsId = '' # <--ここにsinricproに登録したときのDEVICE_IDを設定 deviceIdArr = [blindsId] |
※websockets8.1では動作するが9.1ではエラーとなり動作しないので注意すべし!
1 2 3 4 5 6 7 8 |
sudo apt update sudo apt -y install mosquitto python3 pip python3 -m pip install sinricpro --user python3 -m pip install sinricpro --upgrade --user #when AttributeError: module 'websockets' has no attribute 'client' #python3 -m pip install --force-reinstall websockets==8.1 #Copy paste your credentials into credentials.py python3 sinricpro_connect.py |
※RaspberryPiなどの場合、avahiの設定ファイルとして この内容を登録しておくとmDNSでmqttサーバーアドレスが引けるので便利。
1 2 3 4 5 6 7 8 9 |
<?xml version="1.0" standalone="no"?> <!DOCTYPE service-group SYSTEM "avahi-service.dtd"> <service-group> <name replace-wildcards="yes">%h</name> <service> <type>_mqtt._tcp</type> <port>1883</port> </service> </service-group> |
【修正履歴】
2022-03-09
先日の[TODO]コードの修正が間違っていたので再修正。
2022-03-07
夕方、室内が暗くなってきたとき室内照明を付けると自動でカーテンが閉まるはずが閉まらない?以前に西日対策として追加した[TODO]コードに問題があるとわかったので修正。但し、まだ完全な対策ではないので[TODO]のまま。いつになったら[TODO]が取れるんだろう?(-_-;)
2022-01-25
モーターのラッシュカレントを計測し誤動作する場合があったので電流計測タイミングを変更。
2022-01-23
より強いモーターに交換したため最大電流値変更のための修正、逆起電力回避のためのモーター制御ロジックの修正、ソフトスタート機能(PWM対応)の追加、ライト制御値にヒステリシスを追加、その他バグ修正などのためにcurtain_toso.hを修正。ハードウェアについては下記投稿を参照すべし。
ESP8266(WROOM02)でスマート・カーテン (その2)
2021-12-06
前回の改良の影響でカーテンが自動で開かなくなることがあったため改良。あと、モーターの音がやけにうるさくなってきたようだ。モーターの耐久性が心配だったが想定外に速く駄目になりそうな予感が...そろそろ次のモーター探しをしたほうが良さそうだ。(-_-;)
2021-11-23
実は妻には反対されるだろうなと思いつつも妻のいない時を見計らってカーテンを自動化してしまったのだが、予想通り、妻は開口一番【元に戻して!!】と不満たらたらであったが、ようやく慣れてくれたのか便利さに気が付いてくれたみたいだ。ただ、ルンバ用に持ち上げる機能を設定していると夜でも少し空いてるのが気になっていたらしく夜は完全に閉まっていた方がいいねと言ってくれたので速攻で修正。確かにその通りだ。ユーザーの意見には耳を傾けるべきだね。(/・ω・)/
2021-11-17
長らく利用していたOpenHab1をOpenHab3に移行したついでにalexa連携もSinricProからOpenHabに変更。OepnHab3はarmbian(OrangePi-Zero)BusterへのインストールはZULU11-JDKがインストールできなくて断念してしまったが、Focalはapt install openhabを実行するだけでZULU11-JDKも同時にインストールされるしapt updateも早くてお勧めだ。
ちなみにOpenHab3のログを見ると約1分おきに下記メッセージが出力されるがなんだろう...
[INFO ] [ocontrol.internal.WebSocketConnection] – Web Socket close 1005. Reason: null
2021-11-03
毎日ではないが14:30-15:30頃になると何故かカーテンが閉まってしまうことがある。太陽高度が低い西日が室内を照らすことにより外よりも内のほうが明るいと判断してしまいカーテンを閉めてしまうことが原因だった。外よりも内が明るいというのは夜に照明をつけた場合だけと思っていたのが敗因ではあったものの、考え方によってはそれが正解だったりもしそうな話ではある...とりあえず外がある程度明るいときは外/内の明るさの比較をしないように修正してみた。
2021-10-16
設置当初からときどき夜中に勝手にカーテンが動き出す...こ、これはもしや超常現象というやつか?と少しドキドキしたりもしたが漸く原因が判明。アレクサ連携で使っているSinricProの仕業だった。Pythonで作ったSinricProのサービスが何故か夜中になるとときどき落ちてしまい自動で再起動するのだが、そのときSinricProクラウドは前回の最終操作を再送するという仕様のためだった。
再送は止められないようなのでサービス起動後の15秒間の間に受信した操作要求は無視するようにsinricpro_connect.pyを修正。
これで今夜からは安心して眠れそうだ。(-_-;
2021-09-19
curtain_toso.hを修正。カーテンを閉めるとき最初に逆回転させるのを忘れていたので追加。なくても問題ないんだけど安心安全のためには必要だ。
2021-09-18
curtain_toso.hを修正。カーテンを閉めるときカーテンの状態により途中で引っかかってしまう場合があった。これでたぶん完璧だ。
2021-09-14
esp8266_toso.ino/curtain_toso.hを修正。カーテンを閉める処理をより汎用的に処理できるよう定数値を除去。カーテン開閉状況を取得するためのメソッド名を変更。
2021-09-12
esp8266_toso.ino/curtain_toso.hを修正。カーテンを閉めた後に再度閉める操作をしても無視していたが無視せずに毎回処理するように変更。
カーテン上昇端の設定をしやすくするため現在のカーテン位置情報(秒)をリミット時間の横に表示するように変更。
2021-09-06
curtain_toso.h を修正。カーテンを開けるとき、カーテン上昇端をモーター電流の増加だけで判断していたのを制限時間(motor time limit)経過した場合にも上昇端と判定するように仕様変更。この変更によりカーテンの上昇端を任意の位置にすることが可能となった。
2021-08-19
curtain_toso.h がバグッてたので修正。
【ファームウェア】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
/* esp8266_toso.h - Smart Curtain Firmware for TOSO Twin One Chain Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* ------------------------------------ Arduino 1.8.15 ------------------------------------ esp8266 by ESP8266 Community Version 3.0.1 ------------------------------------ [Board Settings] Board: "Generic ESP8266 Module" Builtin Led: "0" Upload Speed: "115200" CPU Frequency: "80MHz" Crystal Frequency: "26MHz" Flash Size: "4MB (FS:2MB OTA:~1019KB)" Flash Mode: "DOUT (compatible)" Flash Frequency: "40MHz" Reset Method: "no dtr, no_sync" Debug Port: "Disabled" Debug Level: "None" lwIP Variant: "v2 Higher Bandwidth" VTables: "Flash" C++ Exceptions: "Disabled (new aborts on oom)" Stack Protection: "Disabled" Erase Flash: "Only Sketch" Espressif FW: "nonos-sdk 2.2.1+100 (190703)" SSL Support: "All SSL ciphers (most compatible)" MMU: "32KB cache + 32KB IRAM (balanced)" Non-32-Bit Access: "Use pgm_read and macros for IRAM/PROGMEN" */ //#define CURTAIN_DEBUG //#define LEDSENSOR_DEBUG #include <stdint.h> #include <stdbool.h> #include "espwnet.h" #include "ledsensor.h" #include "ledswuart.h" #include "timedata.h" #include "curtain_toso.h" #define PIN_IN1 5 // motor control output1 pin #define PIN_IN2 4 // motor control output2 pin #define PIN_FWD 12 // forward operation switch input pin #define PIN_REV 13 // reverse operation switch input pin #define PIN_ODS 14 // light sensor input pin #define PIN_ADC 16 // adc analog input pin // // MQTT spec // // N: Curtain ID (1,2,3,...) // // [topic] [payload] // /home/curtain/control/inner/N "UP" or "DOWN" or "STOP" // /home/curtain/control/outer/N "UP" or "DOWN" or "STOP" // /home/curtain/state/inner/N 100(closed) - 0(opend) % // /home/curtain/state/outer/N 100(closed) - 0(opend) % // /home/curtain/query/inner/N (query state) // /home/curtain/query/outer/N (query state) // static const String MQTT_CONTROL = "/home/curtain/control/"; static const String MQTT_STATE = "/home/curtain/state/"; static const String MQTT_QUERY = "/home/curtain/query/"; static const String MQTT_INNER = "inner/"; static const String MQTT_OUTER = "outer/"; static LEDSWUART inner_light(PIN_ODS); static LEDSensor<HardwareSerial, Serial> outer_light; static Curtain_TOSO<PIN_IN1, PIN_IN2, PIN_FWD, PIN_REV, PIN_ADC> curtain(&inner_light, &outer_light); static int curtain_status[2][2] = {{-1, -1}, {-1, -1}}; static void addBPSOption(String& html, int bps, int baudrate) { html += F("<option value='"); html += bps; html += F("'"); if (bps == baudrate) html += F(" selected"); html += F(">"); html += bps; html += F("</option>"); } void printTime(String& html, int time) { if (time >= 0) { int h = time / 60; int m = time % 60; if (h < 10) html += '0'; html += h; html += ':'; if (m < 10) html += '0'; html += m; } } static void onCurtainPage(void) { String html; ESPConf& conf = curtain.properties(); ESPWeb.client().setNoDelay(true); // // Save Properties and Restart // if (ESPWeb.arg("apply").equals("apply")) { conf.clear(); conf.setProperty("curtain" , "id" , ESPWeb.arg(F("curtain_id" )).c_str()); conf.setProperty("motor" , "position" , ESPWeb.arg(F("motor_position" )).c_str()); conf.setProperty("motor" , "current_limit" , ESPWeb.arg(F("current_limit" )).c_str()); conf.setProperty("motor" , "time_limit" , ESPWeb.arg(F("time_limit" )).c_str()); conf.setProperty("motor" , "rewind_time" , ESPWeb.arg(F("rewind_time" )).c_str()); conf.setProperty("motor" , "closingup_time" , ESPWeb.arg(F("closingup_time" )).c_str()); conf.setProperty("motor" , "falling_time" , ESPWeb.arg(F("falling_time" )).c_str()); conf.setProperty("motor" , "liftup_time" , ESPWeb.arg(F("liftup_time" )).c_str()); conf.setProperty("time" , "inner_opening" , ESPWeb.arg(F("time_inner_opening")).c_str()); conf.setProperty("time" , "inner_closing" , ESPWeb.arg(F("time_inner_closing")).c_str()); conf.setProperty("time" , "outer_opening" , ESPWeb.arg(F("time_outer_opening")).c_str()); conf.setProperty("time" , "outer_closing" , ESPWeb.arg(F("time_outer_closing")).c_str()); conf.setProperty("time" , "daytime_start" , ESPWeb.arg(F("time_daytime_start")).c_str()); conf.setProperty("time" , "daytime_end" , ESPWeb.arg(F("time_daytime_end" )).c_str()); conf.setProperty("location", "longitude" , ESPWeb.arg(F("location_longitude")).c_str()); conf.setProperty("location", "latitude" , ESPWeb.arg(F("location_latitude" )).c_str()); conf.setProperty("sun" , "inner_opening" , ESPWeb.arg(F("sun_inner_opening" )).c_str()); conf.setProperty("sun" , "inner_closing" , ESPWeb.arg(F("sun_inner_closing" )).c_str()); conf.setProperty("sun" , "outer_opening" , ESPWeb.arg(F("sun_outer_opening" )).c_str()); conf.setProperty("sun" , "outer_closing" , ESPWeb.arg(F("sun_outer_closing" )).c_str()); conf.setProperty("sun" , "daytime_start" , ESPWeb.arg(F("sun_daytime_start" )).c_str()); conf.setProperty("sun" , "daytime_end" , ESPWeb.arg(F("sun_daytime_end" )).c_str()); conf.setProperty("light" , "inner_threshold", ESPWeb.arg(F("inner_threshold" )).c_str()); conf.setProperty("light" , "outer_threshold", ESPWeb.arg(F("outer_threshold" )).c_str()); conf.setProperty("light" , "inner_baudrate" , ESPWeb.arg(F("inner_baudrate" )).c_str()); conf.setProperty("light" , "outer_baudrate" , ESPWeb.arg(F("outer_baudrate" )).c_str()); conf.save(CURTAIN_CONFIGURE_FILE); ESPWNet.onRestartPage("/curtain"); } // // Edit Properties // html = F("<html lang='en'>"); html += F("<head><meta http-equiv='content-type' content='text/html; charset=utf-8'>"); html += F("<meta http-equiv='content-style-type' content='text/css'><style type='text/css'><!--"); html += F("table{border-collapse: collapse}th{background-color: #cccccc; border: solid thin #FFFFFF; padding: 2pt; width: 10em; text-align: left;}"); html += F("td{background-color: #eeeeee; border: solid thin #FFFFFF; padding: 2pt;}--></style><title>Curtain</title></head>"); html += F("<body><form action='/curtain' method='post'><h3>Curtain</h3><table><tbody>"); html += F("<tr><th>ID</th><td><input required type='number' style='text-align:right' name='curtain_id' min='0' max='9999' value='"); html += conf.getProperty("curtain", "id"); html += F("'></tr><tr><th>Rewind Time</th><td><input required type='number' style='text-align:right' name='rewind_time' step='0.1' min='0' max='99.9' value='"); html += conf.getProperty("motor", "rewind_time"); html += F("'> sec</td></tr><tr><th>Closingup Time</th><td><input required type='number' style='text-align:right' name='closingup_time' step='0.1' min='0' max='99.9' value='"); html += conf.getProperty("motor", "closingup_time"); html += F("'> sec</td></tr><tr><th>Falling Time</th><td><input required type='number' style='text-align:right' name='falling_time' step='0.1' min='0' max='99.9' value='"); html += conf.getProperty("motor", "falling_time"); html += F("'> sec</td></tr><tr><th>Liftup Time</th><td><input required type='number' style='text-align:right' name='liftup_time' step='0.1' min='0' max='99.9' value='"); html += conf.getProperty("motor", "liftup_time"); html += F("'> sec</td</tr></tbody></table><h3>Installation Location</h3><table><tbody>"); html += F("<tr><th>longitude</th><td><input type='number' style='text-align:right' name='location_longitude' step='0.000001' min='-179.999999' max='179.999999' value='"); html += conf.getProperty("location", "longitude"); html += F("'> dec</td></tr><tr><th>latitude</th><td><input type='number' style='text-align:right' name='location_latitude' step='0.000001' min='-179.999999' max='179.999999' value='"); html += conf.getProperty("location", "latitude"); html += F("'> dec</td></tr></tbody></table><h3>Opening and Closing"); int sunrise = curtain.sunrise(); int sunset = curtain.sunset(); if ((sunrise >= 0) && (sunset >= 0)) { html += F(" (SunRise "); printTime(html, sunrise); html += F(", SunSet "); printTime(html, sunset); html += F(")"); } html += F("</h3><table><tbody>"); html += F("<tr><th></th><th>Opening Time</th><th>Closing Time</th></tr>"); html += F("<tr><th>Inner Curtain</th><td><input type='time' name='time_inner_opening' value='"); html += conf.getProperty("time", "inner_opening"); html += F("'> <input type='checkbox' name='sun_inner_opening' value='1'"); if (conf.getPropertyInt("sun", "inner_opening")) html += F(" checked"); html += F(">SunRise</td><td><input type='time' name='time_inner_closing' value='"); html += conf.getProperty("time", "inner_closing"); html += F("'> <input type='checkbox' name='sun_inner_closing' value='1'"); if (conf.getPropertyInt("sun", "inner_closing")) html += F(" checked"); html += F(">SunSet</td></tr><tr><th>Outer Curtain</th><td><input type='time' name='time_outer_opening' value='"); html += conf.getProperty("time", "outer_opening"); html += F("'> <input type='checkbox' name='sun_outer_opening' value='1'"); if (conf.getPropertyInt("sun", "outer_opening")) html += F(" checked"); html += F(">SunRise</td><td><input type='time' name='time_outer_closing' value='"); html += conf.getProperty("time", "outer_closing"); html += F("'> <input type='checkbox' name='sun_outer_closing' value='1'"); if (conf.getPropertyInt("sun", "outer_closing")) html += F(" checked"); html += F(">SunSet</td></tr><tr><th>Daytime</th><td><input type='time' name='time_daytime_start' value='"); html += conf.getProperty("time", "daytime_start"); html += F("'> <input type='checkbox' name='sun_daytime_start' value='1'"); if (conf.getPropertyInt("sun", "daytime_start")) html += F(" checked"); html += F(">SunRise</td><td><input type='time' name='time_daytime_end' value='"); html += conf.getProperty("time", "daytime_end"); html += F("'> <input type='checkbox' name='sun_daytime_end' value='1'"); if (conf.getPropertyInt("sun", "daytime_end")) html += F(" checked"); html += F(">SunSet</td></tr></tbody></table><h3>Light Sensor"); sunrise = curtain.light_sunrise(); sunset = curtain.light_sunset(); if (sunrise && sunset) { html += F(" (SunRise "); html += sunrise; html += F(", SunSet "); html += sunset; html += F(")"); } else if (sunrise) { html += F(" (SunRise "); html += sunrise; html += F(")"); } else if (sunset) { html += F(" (SunSet "); html += sunset; html += F(")"); } html += F("</h3><table><tbody><tr><th></th><th>Baudrate</th><th>Threshold (0-65536)</th><tr>"); html += F("<tr><th>Inner Sensor</th><td><select required name='inner_baudrate'>"); int baudrate = conf.getPropertyInt("light", "inner_baudrate"); addBPSOption(html, 115200, baudrate); addBPSOption(html, 57600, baudrate); addBPSOption(html, 38400, baudrate); addBPSOption(html, 19200, baudrate); addBPSOption(html, 14400, baudrate); addBPSOption(html, 9600, baudrate); html += F("</select> bps</td><td><input required type='number' style='text-align:right' name='inner_threshold' min='0' max='65536' value='"); html += conf.getProperty("light", "inner_threshold"); html += F("'> ("); html += curtain.light(true); html += F(")</td></tr><tr><th>Outer Sensor</th><td><select required name='outer_baudrate'>"); baudrate = conf.getPropertyInt("light", "outer_baudrate"); addBPSOption(html, 115200, baudrate); addBPSOption(html, 57600, baudrate); addBPSOption(html, 38400, baudrate); addBPSOption(html, 19200, baudrate); addBPSOption(html, 14400, baudrate); addBPSOption(html, 9600, baudrate); html += F("</select> bps</td><td><input required type='number' style='text-align:right' name='outer_threshold' min='0' max='65536' value='"); html += conf.getProperty("light", "outer_threshold"); html += F("'> ("); html += curtain.light(false); html += F(")</td></tr></tbody></table><h3>Motor</h3><table><tbody>"); html += F("<tr><th>Current Limit</th><td><input required type='number' style='text-align:right' name='current_limit' min='0' max='999' value='"); html += conf.getProperty("motor", "current_limit"); html += F("'> mA</td></tr>"); html += F("<tr><th>Time Limit</th><td><input required type='number' style='text-align:right' name='time_limit' min='0' max='999' value='"); html += conf.getProperty("motor", "time_limit"); html += F("'> sec ("); html += (max(curtain.progress(true), curtain.progress(false)) / 1000); html += F(")</td></tr>"); html += F("<tr><th>Installation Position</th><td>"); int position = conf.getPropertyInt("motor", "position"); html += F("<input type='radio' name='motor_position' value='0'"); if (position == 0) html += F("checked"); html += F(">left "); html += F("<input type='radio' name='motor_position' value='1'"); if (position != 0) html += F("checked"); html += F(">right</td></tr></tbody></table><p><input type='submit' name='apply' value='apply'></p></form></body></html>"); ESPWeb.send(ESPWNET_HTTP_STATUS_OK, F(ESPWNET_HTML_CONTENT_TYPE), html); } static int mqtt_curtain_id(const String& str, bool& inner) { int id = -1; if (str.startsWith(MQTT_INNER)) { id = str.substring(MQTT_INNER.length()).toInt(); inner = true; } else if (str.startsWith(MQTT_OUTER)) { id = str.substring(MQTT_OUTER.length()).toInt(); inner = false; } return id; } static void mqtt_state(bool inner) { String val; val += curtain_status[inner][0]; ESPWNet.publish((MQTT_STATE + (inner ? MQTT_INNER : MQTT_OUTER) + curtain.id()).c_str(), (uint8_t *)val.c_str(), val.length()); } static void mqtt_callback(char *topic, uint8_t *payload, size_t len) { String str = topic; bool inner; int id; // // curtain control // if (str.startsWith(MQTT_CONTROL)) { id = mqtt_curtain_id(str.substring(MQTT_CONTROL.length()), inner); if ((id == 0) || (id == curtain.id())) { str.clear(); str.concat((char *)payload, len); if (str.equalsIgnoreCase("UP" ) || str.equals("0" )) curtain.opening(inner); if (str.equalsIgnoreCase("DOWN") || str.equals("100")) curtain.closing(inner); if (str.equalsIgnoreCase("STOP") || str.equals("OFF")) curtain.stop(); } } // // curtain query // else if (str.startsWith(MQTT_QUERY)) { id = mqtt_curtain_id(str.substring(MQTT_QUERY.length()), inner); if ((id == 0) || (id == curtain.id())) mqtt_state(inner); } } static void mqtt_notify(void) { static uint32_t t; if (millis() - t >= 1000) { t = millis(); for (size_t i = 0; i < sizeof(curtain_status) / sizeof(curtain_status[0]); ++i) { int status = curtain_status[i][0]; if (curtain_status[i][1] != status) { curtain_status[i][1] = status; mqtt_state(i); } } } } static void curtain_callback(bool inner, uint32_t percent) { curtain_status[inner][0] = 100 - percent; } static void restart_callback(void) { curtain.stop(true); } void setup() { int timezone, daylight; // init espwnet ESPWeb.on("/curtain", [](){onCurtainPage();}); ESPWNet.addHtmlRootLink("/curtain", "Curtain"); ESPWNet.setRestartCallback(restart_callback); ESPWNet.setMQTTCallback(mqtt_callback); ESPWNet.subscribe((MQTT_CONTROL + '#').c_str()); ESPWNet.subscribe((MQTT_STATE + '#').c_str()); ESPWNet.begin(); ESPWNet.getTimeZone(&timezone, &daylight); // init curtain curtain.callback(curtain_callback); curtain.begin(timezone, daylight); } void loop() { if (ESPWNet.handle()) mqtt_notify(); curtain.handle(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 |
/* curtain_toso.h - Smart Curtain Library for TOSO Twin One Chain Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __CURTAIN_TOSO_H #define __CURTAIN_TOSO_H #include <stdint.h> #include <stdbool.h> #include <stdlib.h> #include <time.h> #include "espconf.h" #include "suncalc.h" #include "timedata.h" #include "ledsensor.h" #include "Arduino.h" typedef void (*CURTAIN_STATE_CALLBACK)(bool inner, uint32_t percent); #define CURTAIN_CONFIGURE_FILE "curtain.conf" #define CURTAIN_INNER_LIGHT_BAUDRATE 38400 // 9600 - 115200 bps #define CURTAIN_INNER_LIGHT_THRESHOLD 5000 // 0 - 65536 #define CURTAIN_OUTER_LIGHT_BAUDRATE 115200 // 9600 - 115200 bps #define CURTAIN_OUTER_LIGHT_THRESHOLD 5000 // 0 - 65536 #define CURTAIN_MOTOR_POSITION 1 // 0=left, 1=right #define CURTAIN_MOTOR_CURRENT_LIMIT 800 // mA #define CURTAIN_MOTOR_TIME_LIMIT 64 // sec #define CURTAIN_MOTOR_RUSH_CURRENT_TIME 300 // ms #define CURTAIN_REWIND_TIME "0.8" // sec #define CURTAIN_CLOSINGUP_TIME "4" // sec #define CURTAIN_FALLING_TIME "6" // sec #define CURTAIN_LIFTUP_TIME "6" // sec #define CURTAIN_BTN_INTERVAL 20 // ms #define CURTAIN_BTN_LONG_TIME_PRESS 3000 // ms #define CURTAIN_PWM_FREQUENCY 1000 // Hz #define CURTAIN_PWM_RISE_TIME CURTAIN_MOTOR_RUSH_CURRENT_TIME #define CURTAIN_PWM_START_DUTY 30 // % #define CURTAIN_ADC_INTERVAL 10 // ms #define CURTAIN_ADC_RESISTOR 0.20 // Ω #define CURTAIN_HYSTERESIS 10 // % #define CURTAIN_LED_BLINK_INTERVAL 3000 // ms #define CURTAIN_LED_BLINK_ON_TIME 100 // ms #define CURTAIN_CLOSE_MODE 10 enum { CURTAIN_MOTOR_STOP = 0, CURTAIN_MOTOR_FWD = 1, CURTAIN_MOTOR_REV = 2, CURTAIN_MOTOR_BRAKE = 3, }; template<uint8_t IN1PIN, uint8_t IN2PIN, uint8_t FWDPIN, uint8_t REVPIN, uint8_t ADCPIN> class Curtain_TOSO { private: typedef struct { uint32_t progress; TimeData opening; TimeData closing; } curtain_t; ESPConf _sys_config; int _crt_id; LEDSensorBase *_pds_inner; LEDSensorBase *_pds_outer; int _pds_inner_threshold; int _pds_outer_threshold; uint16_t _pds_sunrise; uint16_t _pds_sunset; uint32_t _pds_blink; int _pds_control; uint32_t _adc_start; uint32_t _adc_delay; uint32_t _adc_value; uint32_t _dcm_position; uint32_t _dcm_curr_limit; uint32_t _dcm_time_limit; uint32_t _dcm_rewind; uint32_t _dcm_closingup; uint32_t _dcm_falling; uint32_t _dcm_liftup; uint32_t _dcm_start; uint32_t _dcm_start2; uint32_t _dcm_status; uint32_t _dcm_control[4]; uint32_t _btn_start; uint32_t _btn_pushed; uint32_t _btn_status; curtain_t _crt_control[2]; curtain_t *_crt_current; uint32_t _crt_command; bool _crt_ready; TimeData _day_start; TimeData _day_end; bool _day_time; int _sch_minute; float _loc_longitude; float _loc_latitude; bool _loc_valid; SunCalc _sun_calc; int _sun_timezone; int _sun_daylight; int _sun_yday; int _sun_sunrise; int _sun_sunset; bool _sun_inner_opening; bool _sun_inner_closing; bool _sun_outer_opening; bool _sun_outer_closing; bool _sun_daytime_start; bool _sun_daytime_end; CURTAIN_STATE_CALLBACK _crt_callback; uint32_t mode(bool inner) { return (inner ? CURTAIN_MOTOR_REV : CURTAIN_MOTOR_FWD); } uint32_t reverse(uint32_t mode) { return mode ^ CURTAIN_MOTOR_BRAKE; } curtain_t& curtain(uint32_t mode) { return _crt_control[mode - 1]; } uint32_t motor_current(void) { if (millis() - _adc_start >= _adc_delay) { _adc_start = millis(); _adc_delay = CURTAIN_ADC_INTERVAL; _adc_value = (system_adc_read() * 100) / (int)(CURTAIN_ADC_RESISTOR * 100); #ifdef CURTAIN_DEBUG _adc_delay = 100; Serial.printf("%d mA\r\n", _adc_value); #endif } return _adc_value; } void motor_output(void) { uint32_t duty; switch (_dcm_status) { case CURTAIN_MOTOR_FWD: case CURTAIN_MOTOR_REV: duty = millis() - _dcm_start; if (duty >= CURTAIN_PWM_RISE_TIME) duty = CURTAIN_PWM_RISE_TIME; else duty = (duty * (100 - CURTAIN_PWM_START_DUTY) / 100) + (CURTAIN_PWM_RISE_TIME * CURTAIN_PWM_START_DUTY / 100); analogWrite(_dcm_position ? IN1PIN : IN2PIN, _dcm_status & CURTAIN_MOTOR_FWD ? duty : 0); analogWrite(_dcm_position ? IN2PIN : IN1PIN, _dcm_status & CURTAIN_MOTOR_REV ? duty : 0); break; } } void control(uint32_t mode, bool stop = true) { if ((mode &= CURTAIN_MOTOR_BRAKE) != _dcm_status) { _dcm_status = mode; _dcm_start = millis(); int duty = 0; switch (mode) { case CURTAIN_MOTOR_FWD: case CURTAIN_MOTOR_REV: // delay adc until motor stabilizes _adc_start = _dcm_start; _adc_delay = CURTAIN_MOTOR_RUSH_CURRENT_TIME; _adc_value = 0; break; case CURTAIN_MOTOR_BRAKE: duty = CURTAIN_PWM_RISE_TIME; /* fall through */ default: analogWrite(IN1PIN, duty); analogWrite(IN2PIN, duty); if (stop) motor_command_clear(); break; } } } int after_time(int a, int b) { return a < b ? b : a; } int before_time(int a, int b) { return a > b ? a : b; } void time_handle(void) { time_t now = time(NULL); struct tm lt = *localtime(&now); int year = lt.tm_year + 1900; if ((year >= 2021) && (_sch_minute != lt.tm_min)) { _sch_minute = lt.tm_min; if ((_sun_yday != lt.tm_yday) && _loc_valid) { _sun_yday = lt.tm_yday; _sun_calc.calculate(_loc_longitude, _loc_latitude, lt.tm_yday, year, _sun_timezone, _sun_daylight); _sun_sunrise = _sun_calc.hour(SUNCALC_SUNRISE) * 60 + _sun_calc.minute(SUNCALC_SUNRISE); _sun_sunset = _sun_calc.hour(SUNCALC_SUNSET ) * 60 + _sun_calc.minute(SUNCALC_SUNSET ); } int hhmm = lt.tm_hour * 60 + lt.tm_min; // log light level if (_pds_outer) { if (hhmm == _sun_sunrise) _pds_sunrise = _pds_outer->handle(); if (hhmm == _sun_sunset) _pds_sunset = _pds_outer->handle(); } // int start = _day_start.getTime(); int end = _day_end.getTime(); if (_sun_daytime_start) start = after_time(start, _sun_sunrise); if (_sun_daytime_end) end = before_time(end, _sun_sunset); if (start < 0) start = 0; if (end < 0) end = 24 * 60; _day_time = (start <= hhmm) && (hhmm < end); for (size_t i = 0; i < sizeof(_crt_control) / sizeof(_crt_control[0]); ++i) { curtain_t& curtain = _crt_control[i]; int open = curtain.opening.getTime(); int close = curtain.closing.getTime(); if (i ? _sun_inner_opening : _sun_outer_opening) open = after_time(open, _sun_sunrise); if (i ? _sun_inner_closing : _sun_outer_closing) close = before_time(close, _sun_sunset); if (open == hhmm) { opening(i); ESPLog.printf("Time: Curtain Opening (%s)", i ? "Inner" : "Outer"); ESPLog.add(LOG_INFO); } else if ((close == hhmm) || (_dcm_liftup && ((start == hhmm) || (end == hhmm)))) { closing(i); ESPLog.printf("Time: Curtain Closing (%s)", i ? "Inner" : "Outer"); ESPLog.add(LOG_INFO); } } } } void light_handle(void) { // // led blink control // if (_pds_inner && CURTAIN_LED_BLINK_INTERVAL) { uint32_t t = millis() - _pds_blink; if (t >= CURTAIN_LED_BLINK_INTERVAL) { _pds_blink = millis(); _pds_inner->led(true); } else if (t >= CURTAIN_LED_BLINK_ON_TIME) { _pds_inner->led(false); } } // // curtain control by ambient light // if (_pds_inner && _pds_outer) { uint16_t inner_val = _pds_inner->handle(); uint16_t outer_val = _pds_outer->handle(); if (inner_val && outer_val) { int inner_hysterisis = _pds_inner_threshold * CURTAIN_HYSTERESIS / 100; int outer_hysterisis = _pds_outer_threshold * CURTAIN_HYSTERESIS / 100; // be quiet at night! if (!_crt_ready || !_day_time) _pds_control = 0; // [TODO] west day ? else if ((inner_val < outer_val) && (outer_val < 2000)) ; // dark inner and bright outer else if ((inner_val >= _pds_inner_threshold + inner_hysterisis) && (outer_val < _pds_outer_threshold)) { if (_pds_control != 1) { _pds_control = 1; opening(); ESPLog.printf("Light[%d, %d]: Curtain Opening (Inner)", inner_val, outer_val); ESPLog.add(LOG_INFO); } } // bright inner and dark outer else if ((inner_val < _pds_inner_threshold) && (outer_val >= _pds_outer_threshold + outer_hysterisis)) { if (_pds_control != 2) { _pds_control = 2; closing(); ESPLog.printf("Light[%d, %d]: Curtain Closing (Inner)", inner_val, outer_val); ESPLog.add(LOG_INFO); } } } } } void button_handle(void) { uint32_t now = millis(); if (now - _btn_start >= CURTAIN_BTN_INTERVAL) { _btn_start = now; uint32_t m = CURTAIN_MOTOR_STOP; if (digitalRead(FWDPIN) == 0) m |= CURTAIN_MOTOR_FWD; if (digitalRead(REVPIN) == 0) m |= CURTAIN_MOTOR_REV; if (!_crt_ready) /* ignore. */; else if (_btn_status == CURTAIN_MOTOR_BRAKE) { if (m == CURTAIN_MOTOR_STOP) { control(m); if (now - _btn_pushed >= CURTAIN_BTN_LONG_TIME_PRESS) { motor_command(CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_FWD); motor_command(CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV); } _btn_status = CURTAIN_MOTOR_STOP; } } else if (m == CURTAIN_MOTOR_BRAKE) { control(m); if (_btn_status != m) _btn_pushed = now; _btn_status = m; } else if (m != CURTAIN_MOTOR_STOP) { if (_btn_status != m) _btn_pushed = now; _btn_status = m; } else if (_btn_status != CURTAIN_MOTOR_STOP) { bool inner = (_btn_status == mode(true)); if (now - _btn_pushed < CURTAIN_BTN_LONG_TIME_PRESS) { opening(inner); ESPLog.printf("Button: Curtain Opening (%s)", inner ? "Inner" : "Outer"); ESPLog.add(LOG_INFO); } else { closing(inner); ESPLog.printf("Button: Curtain Closing (%s)", inner ? "Inner" : "Outer"); ESPLog.add(LOG_INFO); } _btn_status = CURTAIN_MOTOR_STOP; } } } void fireStateChangeEvent(uint32_t cmd) { if (_crt_callback) _crt_callback(cmd == mode(true), _crt_current->progress * 100 / _dcm_time_limit); } void motor_command(uint32_t cmd) { bool found = false; for (size_t i = 0; i < sizeof(_dcm_control) / sizeof(_dcm_control[0]); ++i) { if (_dcm_control[i] == CURTAIN_MOTOR_STOP) { if (!found) _dcm_control[i] = cmd; break; } found = (((i == 0) && (_crt_command == cmd)) || (_dcm_control[i] == cmd)); } } void motor_command_clear(void) { for (size_t i = 0; i < sizeof(_dcm_control) / sizeof(_dcm_control[0]); ++i) _dcm_control[i] = CURTAIN_MOTOR_STOP; } void motor_command_shift(void) { size_t i; for (i = 0; i < sizeof(_dcm_control) / sizeof(_dcm_control[0]) - 1; ++i) _dcm_control[i] = _dcm_control[i + 1]; _dcm_control[i] = CURTAIN_MOTOR_STOP; } bool motor_stopping(uint32_t ms) { return (motor_current() >= _dcm_curr_limit) || (millis() - _dcm_start >= ms); } void motor_handle(void) { uint32_t t; switch (_dcm_control[0]) { // // stop // default: control(CURTAIN_MOTOR_STOP); _crt_command = CURTAIN_MOTOR_STOP; _crt_ready = true; break; // // opening the curtain // case CURTAIN_MOTOR_FWD: case CURTAIN_MOTOR_REV: _crt_command = _dcm_control[0]; _crt_current = &curtain(_crt_command); if (_crt_current->progress * 10 >= _dcm_time_limit * 9) motor_command_shift(); else { control(reverse(_crt_command)); _dcm_control[0] = CURTAIN_MOTOR_REV + 1; } break; case CURTAIN_MOTOR_REV + 1: if (motor_stopping(_dcm_rewind >> 1)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_MOTOR_REV + 2: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { control(_crt_command); ++_dcm_control[0]; _dcm_start2 = _dcm_start; } break; case CURTAIN_MOTOR_REV + 3: t = millis() - _dcm_start2; if (t) { _dcm_start2 += t; _crt_current->progress += t; fireStateChangeEvent(_crt_command); } if (motor_stopping(_dcm_time_limit)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_MOTOR_REV + 4: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { control(reverse(_crt_command)); ++_dcm_control[0]; } break; case CURTAIN_MOTOR_REV + 5: if (motor_stopping(_dcm_rewind)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_MOTOR_REV + 6: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) motor_command_shift(); break; // // closing the curtain // case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_FWD: case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV: _crt_command = _dcm_control[0]; _crt_current = &curtain(_crt_command - CURTAIN_CLOSE_MODE); #if 0 if (_crt_current->progress == 0) motor_command_shift(); else #endif { control(reverse(_crt_command - CURTAIN_CLOSE_MODE)); _dcm_control[0] = CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 1; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 1: if (motor_stopping(_dcm_rewind >> 1)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 2: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { control(_crt_command - CURTAIN_CLOSE_MODE); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 3: if (motor_stopping(_dcm_closingup)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 4: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { control(reverse(_crt_command - CURTAIN_CLOSE_MODE)); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 5: if (motor_stopping(_dcm_rewind)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 6: if (motor_stopping(_dcm_falling + CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { control(_crt_command - CURTAIN_CLOSE_MODE); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 7: if (motor_stopping(_dcm_rewind + (_dcm_rewind >> 1))) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 8: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { control(reverse(_crt_command - CURTAIN_CLOSE_MODE)); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 9: if (motor_stopping(_dcm_rewind + (_dcm_rewind >> 1))) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 10: if (motor_stopping(_dcm_falling + CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { _crt_current->progress = 0; fireStateChangeEvent(_crt_command - CURTAIN_CLOSE_MODE); if (_day_time && _dcm_liftup) { control(_crt_command - CURTAIN_CLOSE_MODE); ++_dcm_control[0]; _dcm_start2 = _dcm_start; } else motor_command_shift(); } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 11: t = millis() - _dcm_start2; if (t) { _dcm_start2 += t; _crt_current->progress += t; fireStateChangeEvent(_crt_command - CURTAIN_CLOSE_MODE); } if (motor_stopping(_dcm_liftup)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 12: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) { control(reverse(_crt_command - CURTAIN_CLOSE_MODE)); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 13: if (motor_stopping(_dcm_rewind)) { control(CURTAIN_MOTOR_STOP, false); ++_dcm_control[0]; } break; case CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV + 14: if (motor_stopping(CURTAIN_MOTOR_RUSH_CURRENT_TIME)) motor_command_shift(); break; } } public: Curtain_TOSO(LEDSensorBase *inner = nullptr, LEDSensorBase *outer = nullptr) : _crt_id(0) , _pds_inner(inner) , _pds_outer(outer) , _pds_inner_threshold(0) , _pds_outer_threshold(0) , _pds_sunrise(0) , _pds_sunset(0) , _pds_blink(0) , _pds_control(0) , _adc_start(0) , _adc_delay(0) , _adc_value(0) , _dcm_position(0) , _dcm_curr_limit(0) , _dcm_time_limit(0) , _dcm_rewind(0) , _dcm_closingup(0) , _dcm_falling(0) , _dcm_liftup(0) , _dcm_start(0) , _dcm_start2(0) , _dcm_status(CURTAIN_MOTOR_STOP) , _btn_start(0) , _btn_status(0) , _crt_current(nullptr) , _crt_command(0) , _crt_ready(false) , _day_time(false) , _sch_minute(-1) , _loc_longitude(0) , _loc_latitude(0) , _loc_valid(false) , _sun_timezone(0) , _sun_daylight(0) , _sun_yday(-1) , _sun_sunrise(-1) , _sun_sunset(-1) , _sun_inner_opening(false) , _sun_inner_closing(false) , _sun_outer_opening(false) , _sun_outer_closing(false) , _sun_daytime_start(false) , _sun_daytime_end(false) , _crt_callback(nullptr) { motor_command_clear(); for (size_t i = 0; i < sizeof(_crt_control) / sizeof(_crt_control[0]); ++i) _crt_control[i].progress = 1; } virtual ~Curtain_TOSO(void) { } ESPConf& properties(void) { return _sys_config; } int id(void) { return _crt_id; } int sunrise(void) { return _sun_sunrise; } int sunset(void) { return _sun_sunset; } uint16_t light(bool inner) { LEDSensorBase *obj = inner ? _pds_inner : _pds_outer; return obj ? obj->handle() : 0; } uint16_t light_sunrise(void) { return _pds_sunrise; } uint16_t light_sunset(void) { return _pds_sunset; } void callback(CURTAIN_STATE_CALLBACK cb) { _crt_callback = cb; } void begin(int timezone = 0, int daylight = 0) { uint32_t inner_baudrate, outer_baudrate; pinMode(FWDPIN, INPUT_PULLUP); pinMode(REVPIN, INPUT_PULLUP); pinMode(IN1PIN, OUTPUT); pinMode(IN2PIN, OUTPUT); analogWriteFreq(CURTAIN_PWM_FREQUENCY); analogWriteRange(CURTAIN_PWM_RISE_TIME); _sun_timezone = timezone; _sun_daylight = daylight; // init properties _sys_config.setPropertyInt("curtain" , "id", 0); _sys_config.setPropertyInt("light" , "inner_threshold", CURTAIN_INNER_LIGHT_THRESHOLD); _sys_config.setPropertyInt("light" , "outer_threshold", CURTAIN_OUTER_LIGHT_THRESHOLD); _sys_config.setPropertyInt("light" , "inner_baudrate" , CURTAIN_INNER_LIGHT_BAUDRATE ); _sys_config.setPropertyInt("light" , "outer_baudrate" , CURTAIN_OUTER_LIGHT_BAUDRATE ); _sys_config.setPropertyInt("motor" , "position" , CURTAIN_MOTOR_POSITION ); _sys_config.setPropertyInt("motor" , "current_limit" , CURTAIN_MOTOR_CURRENT_LIMIT ); _sys_config.setPropertyInt("motor" , "time_limit" , CURTAIN_MOTOR_TIME_LIMIT ); _sys_config.setProperty ("motor" , "rewind_time" , CURTAIN_REWIND_TIME ); _sys_config.setProperty ("motor" , "closingup_time" , CURTAIN_CLOSINGUP_TIME ); _sys_config.setProperty ("motor" , "falling_time" , CURTAIN_FALLING_TIME ); _sys_config.setProperty ("motor" , "liftup_time" , CURTAIN_LIFTUP_TIME ); _sys_config.setPropertyInt("sun" , "inner_opening" , 0); _sys_config.setPropertyInt("sun" , "inner_closing" , 1); _sys_config.setPropertyInt("sun" , "outer_opening" , 0); _sys_config.setPropertyInt("sun" , "outer_closing" , 0); _sys_config.setPropertyInt("sun" , "daytime_start" , 1); _sys_config.setPropertyInt("sun" , "daytime_end" , 1); _sys_config.setProperty ("location", "longitude" , ""); _sys_config.setProperty ("location", "latitude" , ""); _sys_config.setProperty ("time" , "inner_opening" , ""); _sys_config.setProperty ("time" , "inner_closing" , ""); _sys_config.setProperty ("time" , "outer_opening" , ""); _sys_config.setProperty ("time" , "outer_closing" , ""); _sys_config.setProperty ("time" , "daytime_start" , ""); _sys_config.setProperty ("time" , "daytime_end" , ""); // load properties _sys_config.load(CURTAIN_CONFIGURE_FILE); // get properties _crt_id = _sys_config.getPropertyInt("curtain", "id", 0); _pds_inner_threshold = _sys_config.getPropertyInt("light" , "inner_threshold", CURTAIN_INNER_LIGHT_THRESHOLD); _pds_outer_threshold = _sys_config.getPropertyInt("light" , "outer_threshold", CURTAIN_OUTER_LIGHT_THRESHOLD); inner_baudrate = _sys_config.getPropertyInt("light" , "inner_baudrate" , CURTAIN_INNER_LIGHT_BAUDRATE ); outer_baudrate = _sys_config.getPropertyInt("light" , "outer_baudrate" , CURTAIN_OUTER_LIGHT_BAUDRATE ); _dcm_position = _sys_config.getPropertyInt("motor" , "position" , CURTAIN_MOTOR_POSITION ); _dcm_curr_limit = _sys_config.getPropertyInt("motor" , "current_limit" , CURTAIN_MOTOR_CURRENT_LIMIT ); _dcm_time_limit = _sys_config.getPropertyInt("motor" , "time_limit" , CURTAIN_MOTOR_TIME_LIMIT ) * 1000; _dcm_rewind = atof(_sys_config.getProperty("motor", "rewind_time" , CURTAIN_REWIND_TIME )) * 1000; _dcm_closingup = atof(_sys_config.getProperty("motor", "closingup_time" , CURTAIN_CLOSINGUP_TIME )) * 1000; _dcm_falling = atof(_sys_config.getProperty("motor", "falling_time" , CURTAIN_FALLING_TIME )) * 1000; _dcm_liftup = atof(_sys_config.getProperty("motor", "liftup_time" , CURTAIN_LIFTUP_TIME )) * 1000; _sun_inner_opening = _sys_config.getPropertyInt("sun" , "inner_opening" , 0) != 0; _sun_inner_closing = _sys_config.getPropertyInt("sun" , "inner_closing" , 1) != 0; _sun_outer_opening = _sys_config.getPropertyInt("sun" , "outer_opening" , 0) != 0; _sun_outer_closing = _sys_config.getPropertyInt("sun" , "outer_closing" , 0) != 0; _sun_daytime_start = _sys_config.getPropertyInt("sun" , "daytime_start" , 1) != 0; _sun_daytime_end = _sys_config.getPropertyInt("sun" , "daytime_end" , 1) != 0; String longitude = _sys_config.getProperty("location" , "longitude"); String latitude = _sys_config.getProperty("location" , "latitude" ); _loc_valid = longitude.length() && latitude.length(); _loc_longitude = atof(longitude.c_str()); _loc_latitude = atof(latitude.c_str()); curtain_t& inner = curtain(mode(true )); curtain_t& outer = curtain(mode(false)); inner.opening.setTime(_sys_config.getProperty("time", "inner_opening")); inner.closing.setTime(_sys_config.getProperty("time", "inner_closing")); outer.opening.setTime(_sys_config.getProperty("time", "outer_opening")); outer.closing.setTime(_sys_config.getProperty("time", "outer_closing")); _day_start .setTime(_sys_config.getProperty("time", "daytime_start")); _day_end .setTime(_sys_config.getProperty("time", "daytime_end" )); if (_pds_inner) _pds_inner->begin(inner_baudrate); if (_pds_outer) _pds_outer->begin(outer_baudrate); motor_command_clear(); motor_command(CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_FWD); motor_command(CURTAIN_CLOSE_MODE + CURTAIN_MOTOR_REV); _crt_ready = false; } void stop(bool force = false) { if (_crt_ready || force) control(CURTAIN_MOTOR_STOP); } void opening(bool inner = true) { if (_crt_ready) motor_command(mode(inner)); } void closing(bool inner = true) { if (_crt_ready) motor_command(mode(inner) + CURTAIN_CLOSE_MODE); } uint32_t progress(bool inner = true) { return _crt_control[inner].progress; } void handle(void) { time_handle(); light_handle(); button_handle(); motor_handle(); motor_output(); } }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
/* ledsensor.h - Optical Detect Sensor Library for ATtiny10 LED Sensor Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __LEDSENSOR_H #define __LEDSENSOR_H #include <stdlib.h> #include <stdint.h> #define LEDSENSOR_LPF 5 // % class LEDSensorBase { private: uint32_t _start; uint16_t _result; uint8_t _filter; uint8_t _count; char _buffer[8]; uint8_t _id; protected: void filter(uint8_t lpf = LEDSENSOR_LPF) { _filter = lpf; } public: LEDSensorBase(uint8_t id = 0) : _start(0) , _result(0) , _filter(LEDSENSOR_LPF) , _count(0) , _id(id) { } virtual ~LEDSensorBase(void) { } uint8_t id(void) { return _id; } virtual void begin(uint32_t baudrate, uint8_t lpf = LEDSENSOR_LPF) = 0; uint16_t handle(void) { for (int c; (c = read()) >= 0; ) { if (c == '\n') { char *endptr; _buffer[_count] = 0; _count = 0; uint32_t val = strtoul(_buffer, &endptr, 16); if ((endptr == _buffer + 4) && (*endptr == '\r')) { // low pass filter _result += (int32_t)(val - _result) * _filter / 100; #ifdef LEDSENSOR_DEBUG Serial.printf("light[%d] = %d\r\n", _id, _result); } else Serial.printf("light[%d] = error.\r\n", _id); #else } #endif } else if (_count < sizeof(_buffer)) _buffer[_count++] = c; } return _result; } virtual int read(void) = 0; virtual void led(bool on) { (void)on; } }; template<typename UART_T, UART_T& UART, uint8_t ID = 1> class LEDSensor : public LEDSensorBase { public: LEDSensor(void) : LEDSensorBase(ID) { } virtual ~LEDSensor(void) { } void begin(uint32_t baudrate, uint8_t lpf = LEDSENSOR_LPF) override { filter(lpf); UART.begin(baudrate); } int read(void) override { return UART.read(); } }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
/* ledswuart.h - Software UART Library for ESP8266/ESP32 Baudrate: ESP8266( 80MHz): Max 230400 bps: (Caution: irregular error occurs) ESP32 (240MHz): Max 57600 bps: (Caution: irregular error occurs) Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __LEDSWUART_H #define __LEDSWUART_H #include <stdint.h> #include <stdbool.h> #include "ledsensor.h" #include "Arduino.h" #define LEDSWUART_NOPIN 0xFF #define LEDSWUART_CLOCK_WIDTH(b) (F_CPU / (b)) #define LEDSWUART_FRAME_NBITS 9 // start(1) + data(8) #if defined(ESP32) #define _DI() portDISABLE_INTERRUPTS() #define _EI() portENABLE_INTERRUPTS() #else #define _DI() noInterrupts() #define _EI() interrupts() #endif class LEDSWUART : public LEDSensorBase { private: uint32_t _bitwidth; uint32_t _frmwidth; uint32_t _endclock; uint32_t _bitclock; uint32_t _bitcount; uint32_t _bitlevel; uint32_t _bitshift; uint8_t _rxputpos; uint8_t _rxgetpos; uint8_t _rxbuffer[256]; // must be 256. uint8_t _rxpin; uint8_t _txpin; bool _rxenable; static void IRAM_ATTR onRxPinChange0(LEDSWUART *instance) { instance->onRxPinChange(ESP.getCycleCount()); } void IRAM_ATTR onRxPinChange(uint32_t clock) { if (!_rxenable) return; if (_bitcount == 0) { _bitclock = clock; _endclock = clock + _frmwidth; _bitcount = 1; _bitlevel = 0; } else { int32_t t = clock - _bitclock + (_bitwidth >> 1); while ((t -= _bitwidth) > 0) { _bitshift = (_bitshift >> 1) | _bitlevel; if (_bitcount++ == LEDSWUART_FRAME_NBITS) { if ((uint8_t)(_rxputpos + 1) != _rxgetpos) _rxbuffer[_rxputpos++] = _bitshift; _bitcount = 0; // start next frame if ((int32_t)(clock - _endclock) >= 0) onRxPinChange(clock); return; } } _bitclock = clock; _bitlevel ^= 0x80; } } void finish(void) { // disable interrupt _DI(); // finish to receiving if (_bitcount && ((int32_t)(ESP.getCycleCount() - _endclock) >= 0)) onRxPinChange(_endclock - 1); // enable interrupt _EI(); } public: LEDSWUART(uint8_t rxpin, uint8_t txpin = LEDSWUART_NOPIN) : LEDSensorBase() , _bitcount(0) , _rxputpos(0) , _rxgetpos(0) , _rxpin(rxpin) , _txpin(txpin) , _rxenable(true) { } virtual ~LEDSWUART(void) { } void begin(uint32_t baudrate, uint8_t lpf = LEDSENSOR_LPF) override { filter(lpf); _bitwidth = LEDSWUART_CLOCK_WIDTH(baudrate); _frmwidth = _bitwidth * (LEDSWUART_FRAME_NBITS + 1); _bitcount = 0; _rxputpos = 0; _rxgetpos = 0; _rxenable = true; if (_rxpin != LEDSWUART_NOPIN) { digitalWrite(_rxpin, LOW); pinMode(_rxpin, INPUT_PULLUP); attachInterruptArg(_rxpin, reinterpret_cast<void (*)(void*)>(onRxPinChange0), this, CHANGE); } if (_txpin != LEDSWUART_NOPIN) { digitalWrite(_txpin, HIGH); pinMode(_txpin, OUTPUT); } } void end(void) { if (_rxpin != LEDSWUART_NOPIN) { detachInterrupt(_rxpin); _rxpin = LEDSWUART_NOPIN; } if (_txpin != LEDSWUART_NOPIN) { pinMode(_txpin, INPUT); digitalWrite(_txpin, LOW); _txpin = LEDSWUART_NOPIN; } } size_t available(void) { finish(); int remain = _rxputpos - _rxgetpos; return remain >= 0 ? remain : remain + sizeof(_rxbuffer); } int read(void) override { return (available() ? _rxbuffer[_rxgetpos++] : -1); } size_t read(uint8_t *buf, size_t len) { size_t cnt = available(); if (cnt < len) len = cnt; else cnt = len; while (len--) *buf++ = _rxbuffer[_rxgetpos++]; return cnt; } void write(uint8_t b) { if (_txpin != LEDSWUART_NOPIN) { // disable interrupt _DI(); // start bit uint32_t clock = ESP.getCycleCount(); digitalWrite(_txpin, LOW); while (ESP.getCycleCount() - clock < _bitwidth) continue; // data bit for (uint8_t i = 1; i; i <<= 1) { digitalWrite(_txpin, b & i); clock += _bitwidth; while (ESP.getCycleCount() - clock < _bitwidth) continue; } // stop bit digitalWrite(_txpin, HIGH); clock += _bitwidth; while (ESP.getCycleCount() - clock < _bitwidth) continue; // enable interrupt _EI(); // spacing to next frame clock += _bitwidth; while (ESP.getCycleCount() - clock < _bitwidth) continue; } } void write(const uint8_t *buf, size_t len) { while (len--) write(*buf++); } void led(bool on) override { if ((_rxpin != LEDSWUART_NOPIN) && (_rxenable == on)) { if (on) { // LED on _rxenable = false; pinMode(_rxpin, OUTPUT); } else { // LED off digitalWrite(_rxpin, HIGH); pinMode(_rxpin, INPUT_PULLUP); digitalWrite(_rxpin, LOW); _rxenable = true; } } } }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
/* timedata.h - Time Data Library Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __TIMEDATA_H #define __TIMEDATA_H #include <stdlib.h> #include <string> class TimeData { private: int _time; public: TimeData(int time = -1) : _time(time) { } virtual ~TimeData(void) { } bool isValid(void) { return _time >= 0; } void setTime(const char *time) { if (time) { std::string str = time; if ((str.length() == 5) && (str[2] == ':')) setTime(atoi(str.substr(0, 2).c_str()), atoi(str.substr(3).c_str())); else setTime(-1); } } void setTime(int hour, int min) { setTime((hour >= 0) && (min >= 0) ? hour * 60 + min : -1); } void setTime(int time) { _time = time; } int getTime(void) { return _time; } std::string& toString(std::string& str) { if (_time >= 0) { int h = _time / 60; int m = _time % 60; if (h < 10) str += '0'; str += h; str += ':'; if (m < 10) str += '0'; str += m; } return str; } }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
/* suncalc.h - SunRise and SunSet Time Calculation Library Caution: This is Approximate formula. There will be a few minutes of error. Copyright (c) 2021 Sasapea's Lab. All right reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __SUNCALC_H #define __SUNCALC_H #include <math.h> #define SUNCALC_PI 3.14159265358979323846 // πの値 #define SUNCALC_DEG(a) ((a) * 180 / SUNCALC_PI) // ラジアンを度に変換するマクロ #define SUNCALC_RAD(a) ((a) * SUNCALC_PI / 180) // 度をラジアンに変換するマクロ typedef enum { SUNCALC_SUNRISE = 0, SUNCALC_SUNSET = 1, } SUNCALC_TYPE; class SunCalc { private: int _year; int _yday; int _hour[2]; int _min[2]; int _sec[2]; protected: /* orignal url: http://radiopench.blog96.fc2.com/blog-entry-735.html Arduinoで日の出・日の入り時刻を計算 近似式を使って日の出・日の入り時刻を求める。 by ラジオペンチ, 2017/5/10, http://radiopench.blog96.fc2.com/ 参考サイト http://k-ichikawa.blog.enjoy.jp/etc/HP/js/sunRise/srs.html http://www.iot-kyoto.com/satoh/2016/01/22/post-99/ */ static float SunRise(float x, float y, int n, int m) // 日の出時刻を求める関数 { float d, e, t; y = SUNCALC_RAD(y); // 緯度をラジアンに変換 d = dCalc(n, m); // 太陽赤緯を求める e = eCalc(n, m); // 均時差を求める // 太陽の時角幅を求める (視半径、大気差などを補正 (-0.899度)) t = SUNCALC_DEG(acos((sin(SUNCALC_RAD(-0.899)) - sin(d) * sin(y)) / (cos(d) * cos(y)))); return (-t + 180 - x) / (360 / 24) - e; // 日の出時刻を返す } static float SunSet(float x, float y, int n, int m) // 日の入り時刻を求める関数 { float d, e, t; y = SUNCALC_RAD(y); // 緯度をラジアンに変換 d = dCalc(n, m); // 太陽赤緯を求める e = eCalc(n, m); // 均時差を求める // 太陽の時角幅を求める (視半径、大気差などを補正 (-0.899度)) t = SUNCALC_DEG(acos((sin(SUNCALC_RAD(-0.899)) - sin(d) * sin(y)) / (cos(d) * cos(y)))); return (t + 180 - x) / (360 / 24) - e; // 日の入り時刻を返す } static float dCalc(int n, int m) // 近似式で太陽赤緯を求める { float d, w; w = (n + 0.5) * 2 * SUNCALC_PI / m; // 日付をラジアンに変換 d = + 0.33281 - 22.984 * cos(w) - 0.34990 * cos(2 * w) - 0.13980 * cos(3 * w) + 3.7872 * sin(w) + 0.03250 * sin(2 * w) + 0.07187 * sin(3 * w); return SUNCALC_RAD(d); // 赤緯を返す(単位はラジアン) } static float eCalc(int n, int m) // 近似式で均時差を求める { float e, w; w = (n + 0.5) * 2 * SUNCALC_PI / m; // 日付をラジアンに換算 e = + 0.0072 * cos(w) - 0.0528 * cos(2 * w) - 0.0012 * cos(3 * w) - 0.1229 * sin(w) - 0.1565 * sin(2 * w) - 0.0041 * sin(3 * w); return e; // 均一時差を返す(単位は時) } /****************************************************************************************/ public: SunCalc(void) : _year(-1) , _yday(-1) { _hour[0] = _min[0] = _sec[0] = -1; _hour[1] = _min[1] = _sec[1] = -1; } virtual ~SunCalc(void) { } void calculate(float longitude, float latitude, int yday, int year, int timezone = 0, int daylight = 0) { if ((_yday != yday) || (_year != year)) { _yday = yday; _year = year; year = (!(year % 4) && (year % 100)) || !(year % 400) ? 366 : 365; int32_t t = SunRise(longitude, latitude, yday, year) * 3600 + timezone + daylight; if (t < 0) t += 86400; _hour[SUNCALC_SUNRISE] = t / 3600 % 24; _min [SUNCALC_SUNRISE] = t / 60 % 60; _sec [SUNCALC_SUNRISE] = t % 60; t = SunSet(longitude, latitude, yday, year) * 3600 + timezone + daylight; if (t < 0) t += 86400; _hour[SUNCALC_SUNSET ] = t / 3600 % 24; _min [SUNCALC_SUNSET ] = t / 60 % 60; _sec [SUNCALC_SUNSET ] = t % 60; } } int hour(SUNCALC_TYPE type) { return _hour[type]; } int minute(SUNCALC_TYPE type) { return _min[type]; } int second(SUNCALC_TYPE type) { return _sec[type]; } }; #endif |
【参照ライブラリ】
ESP8266/ESP32用のネットワーク管理ライブラリ
SinricPro
ArduinoJson by Benoit Blanchon (minimum Version 6.12.0)
WebSockets by Markus Sattler (minimum Version 2.3.5)