MicroPython的午睡(57) ATOMLite、NodeREDからブザーを制御

Joseph Halfmoon

前回は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 を呼び出す上位スクリプト側には以下の変更が必要でした。

    1. 外部デバイスBUZZERのための大域変数の追加
    2. extInit()内でのBUZZERの初期化
    3. callbackSub()内でのサブスクライブ・トピック ”ATOMLite/Buzzer”対応
    4. 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+機で動作しています。

BuzzerFlow
上の方にある、MQTT-inノード “ATOMLite/Button” と “Button” と名のついたダッシュボードのTextノードは、ATOMLiteのボタンの状態(ONかOFFか)を表示するために以前から使用してきたものです。

そのMQTT-inノードからmsg をとりだして “Buzzer OFF”と名付けた以下のchangeノードに接続しました。

ChangeNode上記のように、ボタンのステータスが更新される(何等かの操作がされた)際に、その内容にかかわらず msg.payloadに false を載せて送り出すというだけの機能です。

それを受けるダッシュボードのswitchノード(ダッシュボードでないフローを分岐させるためのswitchノードもあるので間違えないでくだされ)の設定が以下に。

ダッシュボード上に表示されたスイッチをONすれば true を送信し、OFFすればfalse を送信します。そして、入力側から到来した payloadの状態も反映するとともに、そのままスルーで送信します。

DashboardSwitchNode
ATOMLite側でボタンを押してブザーを停止させた場合、巡り巡ってNode-RED側からもOFFの指令が再び届くことになりますが、何か問題が起こるわけでもないので、構わん、というところ。手抜きだな。

さて最後の MQTT-outノードは、新設のトピック ATOMLite/Buzzerに向けて受け取ったmsg.payloadを送信するだけのものです。

MQTTNode

動作確認

複雑化したATOMLiteのダッシュボードの全貌を冒頭のアイキャッチ画像に掲げました。新設のBuzzer用スイッチは先頭に置きました。ONにしたところが以下に。

BuzzerButton鳴りました。ブー、ブーとブサーらしい音で。ダッシュボードのスイッチを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()