chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

flutter 実機用ビルドメモ(workaroundというか)

(実機用にビルドするとエラー、BundleIDがデフォルトのままなのがいけないのか?

It appears that your application still contains the default signing identifier.
Try replacing 'com.example' with your signing id in Xcode:
  open ios/Runner.xcworkspace
Encountered error while building for device.

指定されているファイルではないが、、怒られているcom.exampleはproject.pbxprojにしかないので。。

% pwd
/Users/<usr_name>/lang/flutter/awsiot/ios/Runner.xcodeproj

% grep example project.pbxproj
              PRODUCT_BUNDLE_IDENTIFIER = com.example.awsiot;
              PRODUCT_BUNDLE_IDENTIFIER = com.example.awsiot;
              PRODUCT_BUNDLE_IDENTIFIER = com.example.awsiot;

これらをApple Developerに登録しているBundleIDに替えてみる。

% diff bkup project.pbxproj
298c298
<                               PRODUCT_BUNDLE_IDENTIFIER = com.example.awsiot;
---
>                               PRODUCT_BUNDLE_IDENTIFIER = jp.dxxxxxxk.flutter;
427c427
<                               PRODUCT_BUNDLE_IDENTIFIER = com.example.awsiot;
---
>                               PRODUCT_BUNDLE_IDENTIFIER = jp.dxxxxxxxk.flutter;
450c450
<                               PRODUCT_BUNDLE_IDENTIFIER = com.example.awsiot;
---
>                               PRODUCT_BUNDLE_IDENTIFIER = jp.dxxxxxxk.flutter;

メッセージが変わったものの怒られ続ける。Xcodeを走らせて手当が必要かも
根本原因が分からなかったので、Xcodeを使って一度プロジェクトをコンパイル、正常に動作したので、
コマンドラインに戻って、再度ビルドしたが怒られる(WindowsPCからMacにリモートで入って作業)
直接Macで操作すると正常にビルドできた(リモートだとNGなのは書き込み権限によるのか、環境変数の違いか。。)

以下でビルド

$ flutter build ios

今度は正常にビルドできたので、以下で実機にインストール (デバイス名省略しているので、実行中に聞かれます)

$ flutter install 

この作業で、AWS IoTに接続してスマートメータの瞬時値を1分周期で表示できるスマフォアプリができた。以下がスマフォアプリの画面
前回と変わりませんが、AWS IoT CoreにMQTTで繋いでいる

どれだけ要望あるか分かりませんが、ソースは清書できたらGitLabに上げる予定(今はまだかなり汚いコードなので)
残る作業は、、スマフォから参照したい時に更新リクエストを出せるPublishする機能を作る、AWS IoT CoreのPublishをフックしてLambdaを呼び出してCRONを起動するスクリプトを作る(このままだったら24/365で1分周期でLambdaが動いてPublishするのでコストが無料枠を超えそうで、そもそも誰も見ていない時にPublishしまくるのは無駄)

GitHub CLIで始める快適GitHub生活 - Qiita

Lambdaを使って、Nature社のWebAPIからスマートメータの電力値を取得してIoT CoreにPublishする

Nature Remo WebAPIからスマートメータの計測値を取ってきて、AWS IoT CoreにPublishするLambda用ソース

import json
import urllib.request
import boto3

URL = "https://api.nature.global/1/appliances"
TOKEN = "R6mxxxxxxxxxxxxxxxxxxxxxxkis"
TOPIC = "topic_1"

def AWSIoT_publish(topic, message):
    client = boto3.client('iot-data')
    try:
        client.publish(
            topic = topic,
            payload = message,
            qos = 0
        )
        status = "OK"
    except Exception as e:
        print(e)
        status = "Error"
    return status

def lambda_handler(event, context):
    # TODO implement
    req = urllib.request.Request(URL)
    req.add_header('Authorization', 'Bearer ' + TOKEN )
    smartmeter_instantaneous = None
    with urllib.request.urlopen(req) as response:
        if response.status == 200:
            appliances = json.loads(response.read().decode())
            for property in appliances[0]["smart_meter"]["echonetlite_properties"]:
                if property['epc'] == 231:
                    smartmeter_instantaneous = property
                    AWSIoT_publish(TOPIC, json.dumps(smartmeter_instantaneous))
                    break
        else:
            print(f"Error in connect:{response.status}")
    return {
        'statusCode': 200,
        'body': json.dumps(smartmeter_instantaneous)
    }

1分周期で実行できるようにCloudWatchのEvent Bridgeを使って1分周期の呼び出しを行う

ESP32ではAWSIoTにSubscribeしてサーボを動かし電力値をメータで表示する(以下は試作のメータ・・・)。瞬時値が500W(4/24 16:45時点)であることを示している。電子レンジ等を使うと1000Wまで上がる。

ESP32+MicroPython+MQTTでAWS IoTと接続、メッセージ受信して、サーボを動かす

スマートメータの計測値がいつでも見られるように、ESP32 + Servoでメータを作ろうとしています(基本はおもちゃ)。
まだ清書できていませんが、MicroPythonで書いた、AWS IoT接続→Subscribe→サーボ制御のソースは以下

import machine
import json
from umqtt.simple import MQTTClient

ENDPOINT = b'a3bxxxxxxkf7t-ats.iot.ap-northeast-1.amazonaws.com'
ID = 'basicPubSub'
TOPIC = "topic_1"

KEYFILE = '/certs/priv.key'
CERTFILE = "/certs/cert.pem"

SRV_DUTY_MIN = 35
SRV_DUTY_MAX = 118

power_val = 0        # Smart meter readings

servo_pwm = None
SERVO_PWM_PIN = 12
def servo_setup():
    global servo_pwm
    servo_pwm = machine.PWM(machine.Pin(SERVO_PWM_PIN), freq=50, duty=SRV_DUTY_MAX) 

# value:  0t - 1000
def servo(i):
    global servo_pwm
    servo_pwm.duty(int(SRV_DUTY_MAX - (SRV_DUTY_MAX - SRV_DUTY_MIN)*i / 1000))

def _cb(topic, msg):
    global power_val
    print("-------- call back ----------")
    print("topic:{}".format(topic))
    print("msg:{}".format(msg))
    print("-----------------------------")
    val = json.loads(msg.decode())['val']
    power_val = int(val)
    if power_val >= 1000:
       power_val = 999

#
#
#
with open(KEYFILE, 'r') as f:
    key = f.read()

with open(CERTFILE, 'r') as f:
    cert = f.read()

# SSL certificates.
SSL_PARAMS = {'key': key, 'cert': cert, 'server_side': False}
client = MQTTClient(client_id=ID, server=ENDPOINT, port=8883, ssl=True, ssl_params=SSL_PARAMS)

client.set_callback(_cb)
client.connect()
client.subscribe(topic=TOPIC)

servo_setup()

while True:
   print("---loop---------------")
   client.wait_msg()
   print("power val:{}".format(power_val))
   servo(power_val)

長時間動かすと、wait_msgでExceptionが発生する問題があるが、スマートメータで計測した値がPublishされた時に、サーボのアームが動くところまではできた。(瞬時値:0W~1000Wまで連動)
スマートメータの瞬時値と積算値はNature Remo E Liteで取得してNature社のサーバに上がっているので、Nature社のAPIを叩いて、AWS IoTにPublishする。配信されるメッセージの電文

'{"name": "measured_instantaneous", "epc": 231, "val": "240", "updated_at": "2022-04-23T12:26:13Z"}'

今後の取り組み

  1. Nature社のAPIを叩いてPublishするプログラムがRPi上で動作しているので、そのプログラムをAWS Lambdaに移す
  2. 上記Lambdaを定期的に呼び出すCRONを仕込む
  3. FlutterでMQTT試作したが、接続先をAWS IoT Coreに変更する
  4. 24時間CRONが走るのは無駄で、お金がかかってしょうがないので、Client接続がある時だけCRONが走るようにする
  5. 開発終わり

■追記
長時間運用すると、セッションが遮断されるのか、wait_msg()でExceptionが発生する(本当に接続がきれるのか?は未調査)。場当たり的だが、Exceptionが発生したら自分でリセットするように仕込む

while True:
   print("---loop---------------")
   try:
       client.wait_msg()
       print("power val:{}".format(power_val))
       servo(power_val)
   except Exception as e:
       print("Except at wait_msg")
       print(e)
       print("restart")
       machine.reset()       

REPLで貼り付けるだけだと、リセットがかかると全部消えるので、上記プログラムをflash上のファイル main.pyとして書き込むことで、再起動後自動的に実行される。

AWS IoTを介して RPiとESP32を接続

全てはスマートメータのための泥沼なのだが、、AWS IoTを介して、RPiとESP32をPub/Subで接続する。
メッセージの発行はRPi側で、ESP32は購読のみ。
RPi側のソース

#!/usr/bin/python

import paho.mqtt.client as mqtt
import json
import ssl

host = 'a3bxxxxxkf7t-ats.iot.ap-northeast-1.amazonaws.com'
port = 8883
cacert = './cert/AmazonRootCA1.pem'
clientCert = './cert/16f31xxxxxxxxxxxxxxx324f-certificate.pem.crt'
clientKey = './cert/16f31xxxxxxxxxxxxxxxx324f-private.pem.key'

id = "sensor01"

def _connect(client, userdata, flags, respons_code):
    print("connected")

def _publish(client, userdata, mid):
    client.disconnect()
    
client = mqtt.Client(client_id= id, protocol=mqtt.MQTTv311)
client.tls_set(cacert,
        certfile = clientCert,
        keyfile = clientKey,
        tls_version = ssl.PROTOCOL_TLSv1_2)

client.tls_insecure_set(True)
client.on_connect = _connect
client.on_publish = _publish
client.connect(host, port=port, keepalive=6)

topic = 'topic_1'
msg = "hello from RPi"

import time
while True:
   time.sleep(5)
   print("-----------------------------------------")
   print(f"publish msg:{msg}")
   client.publish(topic, json.dumps({"msg" : msg}), qos=1)

EPS32側のソース(バグがあるようでMQTTの接続が切れる)

from umqtt.simple import MQTTClient
import json

ENDPOINT = b'axxxxxxxx7t-ats.iot.ap-northeast-1.amazonaws.com'
ID='basicPubSub'

def _cb(topic, msg):
    print("call back")
    print(topic)
    print(msg)

keyfile = '/certs/private.pem.key'
with open(keyfile, 'r') as f:
    key = f.read()

certfile = "/certs/certificate.pem.crt"
with open(certfile, 'r') as f:
    cert = f.read()

# SSL certificates.
SSL_PARAMS = {'key': key, 'cert': cert, 'server_side': False}
client = MQTTClient(client_id=ID, server=ENDPOINT, port=8883, ssl=True, ssl_params=SSL_PARAMS)

client.set_callback(_cb)
gc.collect()
client.connect()
client.subscribe(topic="topic_1")

while True:
   client.wait_msg()
   print("------------------")

気休めだけど、connect()の実行前に、GCを走らせたら安定している(今は)。GCした場合、しなかった場合でもう少しどう変わるか確認が必要
接続が切れるのは、Pahoで実装したRPi側のPublishしている方だった。RPiだからリソースも十分なはずなのに。。
以下はどうにかこうにか動いている画面(上がRPi、下がESP32)

Publishするだけなら、MQTTでなくても、AWS IoTが提供しているHTTPS/POSTでいいのだが。。

■追記
日が変わって製作を再開したが、昨日は出なかったエラーが出る。ESP32側なのでかなり厄介(代替え手段がほとんどない、デバッグ手段がない)。ソースを確認するか。。

Traceback (most recent call last):
  File "<stdin>", line 31, in <module>
  File "umqtt/simple.py", line 184, in wait_msg
OSError: -1
>>>

micropython-lib/simple.py at master · micropython/micropython-lib · GitHub

    def wait_msg(self):
        res = self.sock.read(1)
        self.sock.setblocking(True)
        if res is None:
            return None
        if res == b"":
            raise OSError(-1)

エラーが出ている原因は、sock.read(1)で""が返却されるため。""が返るのはどんな時なのか。。sock.read(1)の正しい動きは、データか、NULLが返却される仕様なんだろう。""が返るのはsessionがcloseされているからかもしれない。
CloudWatchのログを確認すると、ClientIDの重複エラーが発生している。AWSサーバ(MQTT ブローカ)側から通信を遮断したと考えられる。

{
    "timestamp": "2022-04-23 07:36:21.348",
    "logLevel": "ERROR",
    "traceId": "016a2392-d772-e030-456b-399da25017a9",
    "accountId": "365701690774",
    "status": "Failure",
    "eventType": "Disconnect",
    "protocol": "MQTT",
    "clientId": "basicPubSub",
    "principalId": "16f31f1aaffde60f2ac508f99b9604b3aaf601c6531534fc99919d52aa83324f",
    "sourceIp": "121.118.240.85",
    "sourcePort": 57235,
    "reason": "DUPLICATE_CLIENT_ID",
    "details": "A new connection was established with the same client ID",
    "disconnectReason": "DUPLICATE_CLIENTID"
}

IDを変えれば解決するはず。昨日はどうだったのか?? 昨日は異なるIDを使っている。昨日と今日とでは、一部ソースが変わってしまっていて、ID重複を起こしてしまったようだ。何も変えていないつもりが変わっているという。。

mqtt通信エラーの原因・・mqttライブラリでAWS IoT Coreに接続できないのは、許可されていないIDを設定していたため

MicroPythonの標準ライブラリのmqtt.simpleを使ってなぜAWS IoT Coreに接続できないのかずっと悩んでいた。
結果としては、許可されていないIDを指定して接続しようとして認証エラーになっていた。

client_id = 'basicPubSub' だと正常なのだが、当初は、IDを適当に 'esp32_0012'等を指定していてずっとエラーだった。(正確には無応答)
AWS IoT SDKのサンプルでは、IDとして'basicPubSub'を使っていて、このIDだと正常に接続できるのが分かった。
正常接続時のログ(CloudWatch) ("clientId": "basicPubSub")

{
    "timestamp": "2022-04-21 14:40:06.824",
    "logLevel": "INFO",
    "traceId": "0eba8669-bd79-faf6-ff62-f7bf66cbae27",
    "accountId": "36000000000000074",
    "status": "Success",
    "eventType": "Subscribe",
    "protocol": "MQTT",
    "topicName": "topic_1",
    "clientId": "basicPubSub",
    "principalId": "16f31f1aaffde60f2ac508f99b9604b3aaf601c6531534fc99919d52aa83324f",
    "sourceIp": "2400:4150:5060:7900:dea6:32ff:fed3:72ee",
    "sourcePort": 60431
}

接続異常時のログ(認証エラー)( "clientId": "python1")

{
    "timestamp": "2022-04-21 15:03:09.905",
    "logLevel": "ERROR",
    "traceId": "e11e02ca-342a-a573-7999-9adec3af4aad",
    "accountId": "360000000000074",
    "status": "Failure",
    "eventType": "Connect",
    "protocol": "MQTT",
    "clientId": "python1",
    "principalId": "16f31f1aaffde60f2ac508f99b9604b3aaf601c6531534fc99919d52aa83324f",
    "sourceIp": "2400:4150:5060:7900:dea6:32ff:fed3:72ee",
    "sourcePort": 56783,
    "reason": "AUTHORIZATION_FAILURE",
    "details": "Authorization Failure"
}

しかし、id:basicPubSubってデフォルトで許可されているIDなんだろうか。。Client証明書のSubjectがbasicPubSubとか??
Client証明書を調べてみたが、そんな単純は話ではなかった。

$ openssl x509 -text   -in cert.pem.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Issuer: OU = Amazon Web Services O=Amazon.com Inc. L=Seattle ST=Washington C=US
        Validity
            Not Before: Apr 18 23:26:09 2022 GMT   Not After : Dec 31 23:59:59 2049 GMT
        Subject: CN = AWS IoT Certificate

IDの制約は証明書に紐づくPolicyで管理されているのが分かった(過去にそう理解していたが、設定の終わったAWS IoT Coreを使いまわししていたので忘れていた)
以下は証明書に紐づくPolicy、Policy内に接続許可IDを指定している(昔に設定したのだろうけど忘れていた)

上記Pocilyで動いているなら、basicPubSub以外に、sensor01~sensor03も使えるはず。

■おまけ

原因が分からずmqttライブラリをばらしてどこでエラーになるのか、一行ずつ叩いて調べていた。以下はmqttライブラリをばらして書いたMQTT Clientの例 (connectまで)

import usocket as socket
import ussl

ENDPOINT = 'a3bxxxxxxxkf7t-ats.iot.ap-northeast-1.amazonaws.com'
PORT = 8883                        
client_id = 'basicPubSub'          # OK

CERT="certs/certificate.pem.crt"
KEY="certs/private.pem.key"

def msg_gen(id):
    clean_session = 1
    keepalive = 60
    buf = [0]*40
    buf[0] = 0x10
    buf[1] = len(id) + 10 + 2
    buf[3] = 4
    buf[4] = ord('M')
    buf[5] = ord('Q')
    buf[6] = ord('T')
    buf[7] = ord('T')
    buf[8] = 4
    buf[9] = clean_session << 1
    buf[10] = (keepalive >> 8) & 0xff
    buf[11] = keepalive & 0xff
    buf[13] = len(id)
    idx = 14
    for chr in id:
        buf[idx] = ord(chr)
        idx += 1
    return bytes(buf[0:idx])

with open(KEY, 'r') as f:
    key = f.read()

with open(CERT, 'r') as f:
    cert = f.read()

# SSL certificates.
ssl_params={"cert": cert, "key": key, 'server_side': False}

addr = socket.getaddrinfo(ENDPOINT, PORT)[0][-1]

clean_session = 1   # set 0 or 1
sock = socket.socket()
sock.connect(addr)

sock = ussl.wrap_socket(sock, **ssl_params)
sock.write(msg_gen(client_id))

print("===ret============")
sock.read()
print("==================")

■追記
なぜ使えるIDがbasicPubSubなのかをググっていたら、2020年の自分の記事に出くわした。アクセス許可ファイルに、id: basicPubSubが許可設定されているらしい。当時はAWS IoT Coreを使い始めた時期だったので、自分で設定したようだった。今となっては完全に忘れている。
AWS IoT Coreを使ってみる (デバイスからMQTTでPub/Sub) - chakokuのブログ(rev4)

Micropythonを使ってAWS IoTのMQTT(8883) に connectできない

世の中のMQTT 接続のライブラリソースはほぼ1種類なのだが、どうやってもAWS IoT のMQTT(8883)に接続できない。sockで通信経路は確立するようなのだが、AWSからの応答がない。なぜなのか。。
AWS IoTと正しく通信できないという書き込みはあちこちにあるのだけど、、ファームバージョンによって、うまくいったというレポートもあり。
MQTT to AWS IoT Core fails with mbedtls_ssl_handshake_error · Issue #5929 · micropython/micropython · GitHub

I have good news and I have bad news! The new version of MicroPython, 
idf4 v1.15 -- esp32-20210418-v1.15.bin, works with AWS IoT! YAY!!!

v1.15 does not have mqtt built in so you have to install your own. 
That's OK because it's easy to do.

2021/4時点の情報で、 idf4 V1.15なら動作したと書かれている。