前回はM5ATOMLiteに接続したブザーをPWMで鳴らせるようにしました。今回は、ブラウザ画面からNode-REDダッシュボードを操作して、そのブザーを鳴らしてみようと思います。鳴らしたままだとウルサイので止める機能も仕込んでありますよ。それにしてもATOMLite用のダッシュボード、項目増えすぎか。
※「MicroPython的午睡」投稿順 Indexはこちら
(実験に使用したMicroPythonスクリプトの全文は末尾に。使用しているMicroPythonインタプリタはESP32用のgeneric port<ESP32機なら多分どれでも走る特殊化していないバージョン>です。ESP32上のMicroPythonについてはこちらのドキュメント<日本語>をご参照いただくのがよろしいかと思います。)
新たなMQTTサブスクライブTOPICに対応するための変更
Node-REDのダッシュボード画面からMQTTブローカ経由でATOMLiteに、ブザーのON/OFFを指示するにあたり、適当なサブスクライブ・トピックが無いことに気づきました。ブザー制御用にトピックを追加しようとしたのですが、こんどはATOMLiteの/lib内に書きこみ済の以下のモジュールを変更しないとイケないことに気づきました。
/lib/LiteNetwork.py
今後もトピックの追加はあると思うので、毎回、下位のモジュールを変更するのは避けたいです。そこで、サブスクライブするトピックを上位スクリプトからリストで与えるようにインタフェースを変更しました。こんな感じ。
def connectMqttBroker(self, callbackFuncName, topic): try: self.callbackSub = callbackFuncName self.client = umqtt.simple.MQTTClient(self.clientId, self.brokerAdr, port=self.brokerPort) self.client.set_callback(self.callbackSub) self.client.connect() for item in topic: self.client.subscribe(item) except: return False return True
これでしばらく変更しなくても済むかね(根拠の無い期待。。。)
呼び出し側のmain()関数は以下のように変えました。ずらずらとトピックを並べて呼び出すだけ。
def main(): if not LiteNet.do_connect(): print("ERROR: WiFi connection.") sys.exit(1) LiteNet.initRTC() subscribeTopic = ["ATOMLite/Color", "ATOMLite/Settings", "ATOMLite/Buzzer"] if not LiteNet.connectMqttBroker(callbackSub, subscribeTopic): print("ERROR: MQTT Broker connection.") sys.exit(1) extInit() uasyncio.run(mainLoop(1000))
ATOMLite側のBuzzer制御
前回パッシブブザーをPWM波形で鳴らすためのハードウエア制御部分 /lib/LiteBUZ.py を作りました。今回はそれに変更はありません。良かった。
しかし、モジュール LiteBUZ.py を呼び出す上位スクリプト側には以下の変更が必要でした。
-
- 外部デバイスBUZZERのための大域変数の追加
- extInit()内でのBUZZERの初期化
- callbackSub()内でのサブスクライブ・トピック ”ATOMLite/Buzzer”対応
- ledAndUserButton()内でのBuzzerのON/OFF制御
1,2は、外部デバイスを追加する度に(例えばBMP280センサ)やってきた作業なので説明省略します。末尾のソースをご覧ください。
3は、MQTTのサブスクライブ・トピック “ATOMLite/Buzzer”に反応するためのCallBack関数内での対応です。トピックが到着し、その値が true ならブザーを鳴らすモードにし、false ならブザー停止モードにするというだけ。ここではモードの設定だけで実際にブザーを鳴らしたりはしません。
4が、実際にブザーを鳴らしている場所です。ATOMLite内蔵のLEDとボタンを制御するために使われている関数です。この関数は、asyncioを使って、毎秒1回起動されるようになっている「周期タスク」です。LEDはこれを使って点滅(毎秒トグル)しています。同じように、ブザーを鳴らすモード時には、ブザーも1秒鳴っては1秒停止を繰り返すようにしました。ダラダラ連続で鳴り続けるよりも人の意識を引きやすいと思ったからです。ブー、ブーとなります。
勿論、ダッシュボード側で「ブザーのスイッチを切れ」ば3のルートでブザー停止モードになるので、ブザーは停止します。しかし、ダッシュボード画面は遠くに(もしかすると地球の裏側に)あるかもしれないので、鳴っているブザーが「ウルサイ」と現場で切れる機能が欲しかったです(実際かなり耳障りに鳴ります。)そのため、1個しかないボタンを操作したらブザーは切れる仕様といたしました。
ATOMLite側でブザーをローカルに停止させて、Node-RED側に何も報告しないとダッシュボードの表示と現物の動作に齟齬が生じてしまいます。しかしボタン操作は4の関数内で以前から報告されています。同じ4の関数にブザー制御も入ったので、これに便乗すれば簡単。何時もながら安直すぎるな。ボタン操作を知ったダッシュボード側であとはよしなに、と。
Node-REDフロー
さてNode-RED側のフローの改造部分が以下に。なお、Node-REDサーバーとMQTTブローカ(Mosquitto)は Raspberry Pi 3 model B+機で動作しています。
上の方にある、MQTT-inノード “ATOMLite/Button” と “Button” と名のついたダッシュボードのTextノードは、ATOMLiteのボタンの状態(ONかOFFか)を表示するために以前から使用してきたものです。
そのMQTT-inノードからmsg をとりだして “Buzzer OFF”と名付けた以下のchangeノードに接続しました。
上記のように、ボタンのステータスが更新される(何等かの操作がされた)際に、その内容にかかわらず msg.payloadに false を載せて送り出すというだけの機能です。
それを受けるダッシュボードのswitchノード(ダッシュボードでないフローを分岐させるためのswitchノードもあるので間違えないでくだされ)の設定が以下に。
ダッシュボード上に表示されたスイッチをONすれば true を送信し、OFFすればfalse を送信します。そして、入力側から到来した payloadの状態も反映するとともに、そのままスルーで送信します。
ATOMLite側でボタンを押してブザーを停止させた場合、巡り巡ってNode-RED側からもOFFの指令が再び届くことになりますが、何か問題が起こるわけでもないので、構わん、というところ。手抜きだな。
さて最後の MQTT-outノードは、新設のトピック ATOMLite/Buzzerに向けて受け取ったmsg.payloadを送信するだけのものです。
動作確認
複雑化したATOMLiteのダッシュボードの全貌を冒頭のアイキャッチ画像に掲げました。新設のBuzzer用スイッチは先頭に置きました。ONにしたところが以下に。
鳴りました。ブー、ブーとブサーらしい音で。ダッシュボードのスイッチをOFFにすると鳴りやみます。再びダッシュボードでON、ブーブー。今度はATOMLiteのボタンを押してみます。鳴りやみました。そしてダッシュボードのBuzzerスイッチもOFFになってます。
たかがスイッチ1個だけれでも結構メンドイですな。
MicroPython的午睡(56) ATOMLite、パッシブ・ブザー接続 へ戻る
MicroPython的午睡(58) micro:bit v2、MicroPython書込 へ進む
今回実験に使用したMicroPythonスクリプト(最上位)全文
import sys import ujson import uasyncio from LiteOnBoard import LiteOnBoard from LiteNetwork import LiteNetwork from LiteDHT11 import LiteDHT11 from LiteBMP280 import LiteBMP280 from LiteCds import LiteCDS from LiteBUZ import LiteBUZ ledColor = (0, 0, 0) BuzzerMode = False OnBoardDevice = LiteOnBoard() LiteNet = LiteNetwork() DHT11 = None BMP280 = None CDS = None BUZZER = None DHT11_ON = 0x1 BMP280_ON = 0x2 CDS_ON = 0x4 BUZZER_ON = 0x8 extDevControl = BMP280_ON | DHT11_ON | CDS_ON | BUZZER_ON def colorChange(msg): global ledColor if msg=="R": ledColor = (255, 0, 0) elif msg=="G": ledColor = (0, 255, 0) elif msg=="B": ledColor = (0, 0, 255) def callbackSub(topic, msg): global BuzzerMode topicS = topic.decode('utf-8') msgS = msg.decode('utf-8') print(topicS, msgS) if topicS=="ATOMLite/Color": print("ATOMLite/Color Message: ", msgS) colorChange(msgS) if topicS=="ATOMLite/Settings": msgJ = ujson.loads(msgS) print("ATOMLite/Settings: ") for item in msgJ: for k,v in item.items(): if k=="key": ky = v if k=="value": vl = v print(ky, " = ", vl) if topicS=="ATOMLite/Buzzer": print("ATOMLite/Buzzer Message: ", msgS) if msgS=="true": print("BuzzerMode: True") BuzzerMode = True else: print("BuzzerMode: False") BuzzerMode = False def makeOBJ(num): global DHT11, BMP280, CDS workDic = dict() if (extDevControl & DHT11_ON) != 0: tpl = DHT11.measure() workDic['TEMP'] = tpl[0] workDic['HUMI'] = tpl[1] if (extDevControl & BMP280_ON) != 0: tpl = BMP280.measure() workDic['BMP280_T'] = tpl[0] workDic['BMP280_P'] = tpl[1] if (extDevControl & CDS_ON) != 0: tpl = CDS.measure() workDic['CDS_RAW'] = tpl[0] workDic['A'] = num workDic['B'] = LiteNet.currentTIME() return ujson.dumps(workDic) async def reportStatus(period_s): while True: LiteNet.client.publish("ATOMLite/Status", "ATOMLite Running: " + LiteNet.currentTIME()) await uasyncio.sleep(period_s) async def sendJson(period_s): sendCounter = 0 while True: jsonSTR = makeOBJ(sendCounter) LiteNet.client.publish("ATOMLite/Json", jsonSTR) sendCounter += 1 await uasyncio.sleep(period_s) async def ledAndUserButton(period_s): global OnBoardDevice, BuzzerMode, BUZZER ledFlag = True buttonFlag = False buzzerFlag = True while True: if ledFlag: OnBoardDevice.M5ATOMLiteLED(ledColor) ledFlag = False else: OnBoardDevice.M5ATOMLiteLED( (0, 0, 0) ) ledFlag = True if (BUZZER is not None) and BuzzerMode and buzzerFlag: BUZZER.start() buzzerFlag = False elif (BUZZER is not None) and (not buzzerFlag): BUZZER.stop() buzzerFlag = True else: buzzerFlag = True if OnBoardDevice.M5ATOMLiteUserButton(): print("Button ON") LiteNet.client.publish("ATOMLite/Button", "Button ON") buttonFlag = True BuzzerMode = False elif buttonFlag: buttonFlag = False LiteNet.client.publish("ATOMLite/Button", "Button OFF") await uasyncio.sleep(period_s) async def mainLoop(period_ms): global OnBoardDevice taskStatus = uasyncio.create_task(reportStatus(60)) taskJson = uasyncio.create_task(sendJson(60)) taskATOMhard = uasyncio.create_task(ledAndUserButton(1)) while( OnBoardDevice.buttonCounter < 6 ): LiteNet.client.check_msg() await uasyncio.sleep_ms(period_ms) try: taskStatus.cancel() except: print("Exception at taskStatus:", sys.exc_info()[0]) try: taskJson.cancel() except: print("Exception at taskJson:", sys.exc_info()[0]) try: taskATOMhard.cancel() except: print("Exception at taskATOMhard:", sys.exc_info()[0]) LiteNet.client.publish("ATOMLite/Button", "N/A") LiteNet.client.publish("ATOMLite/Status", "ATOMLite: out of service.") LiteNet.close() print("disconnect.") def extInit(): global DHT11, BMP280, CDS, BUZZER if (extDevControl & DHT11_ON) != 0: DHT11 = LiteDHT11() print("Device: DHT11, active.") if (extDevControl & BMP280_ON) != 0: BMP280 = LiteBMP280() BMP280.setup() print("Device: BMP280, active.") if (extDevControl & CDS_ON) != 0: CDS = LiteCDS() print("Device: CDS, active.") if (extDevControl & BUZZER_ON) != 0: BUZZER = LiteBUZ() print("Device: BUZZER, active.") def main(): if not LiteNet.do_connect(): print("ERROR: WiFi connection.") sys.exit(1) LiteNet.initRTC() subscribeTopic = ["ATOMLite/Color", "ATOMLite/Settings", "ATOMLite/Buzzer"] if not LiteNet.connectMqttBroker(callbackSub, subscribeTopic): print("ERROR: MQTT Broker connection.") sys.exit(1) extInit() uasyncio.run(mainLoop(1000)) if __name__ == "__main__": main()