Raspberry Pi PicoWでLINEに通知できない問題を検証する

Raspberry Pi PicoWを使った見守りシステムを構築しようと悪戦苦闘しています。
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com
一つ一つの機能を検証して問題なく動作することを確認して、いざ、それらを組み合わせて、実際のシステムとして運用しようとすると、PCに接続した状態ではちゃんと動くのに、単独動作では動かないという問題にぶち当たりました。
じゃあ、似たようなハードでESP32なら動作するのかということで、ESP32開発ボードを使って検証すると、こちらは何の問題もなく動きました。
alasixosaka.hatenablog.com
alasixosaka.hatenablog.com


今回は、再びRaspberry Pi PicoW(ラズパイピコW)に戻って、検証を続けた結果です。

現象のおさらいと今までに試してみたこと

今回の検証の記事を書く前に、遭遇した現象のおさらいと、いくつか試してみた検証の結果をおさらいしておきます。
現象は

  • LINEの通知がうまくいかない
  • シリアル通信の回数のカウントが0から始まるところが1になってしまう
  • なぜか突然停止する

2番目については少し補足が必要だと思いますので、補足を書いておきます。プログラムではTWELITEからの通信があった時(つまりシリアルポートにデータ送られてきたとき)の回数をカウントしていて、プログラム開始時は当然0回、1回、2回と数えていき、24時間経過したらゼロにリセットしてまた0からカウントするようになっています。このカウントをThingsPeakに1時間おきに送信します。つまりカウントがあれば、その家で人の動きがあるということなので心配しなくてもよいということになります(家の中で犬や猫などのペットを飼っている家ではこの方法は使えないです。檻にこの子機をつけておいて、ちゃんと生きているか確認するという方法には使えると思いますが)。
そして、24時間経過してもカウントがゼロのままだったら、つまりまる1日何の動きもなかった場合は異常としてLINEにアラートを出すようにしています。なので、始めはカウントはゼロのはずなんですが、何故か単独で動かすと初めは1になります。PCと繋いでThonnyから動かすとちゃんとゼロからスタートします。
まあ、これはまだ軽症なのですが、LINEの通知が行かないのは、アラートを出している意味がないのでちょっと困りますし、見守りシステムですから自宅から離れたところに設置するわけで、ずっと動き続けてくれないと困りますので、3番目も致命的と言えます。

今までの検証

どうもおかしいということで、電源周りを疑ってみて、USB経由での電源供給をやめて、直接DCをチップに入れてみたり、ACアダプタをもっと容量の大きいのに変えてみたりしましたが問題は解決しませんでした。

リセットスイッチをつけてみる

ここからが今回の検証になりますが、まず、リセットスイッチを外付けでつけてみることにしました。
ラズパイピコwにはリセットスイッチがありません。シリアル通信の回数が初めから1回になるということは、起動時に何らかのデータがシリアルのバッファに残っているためと考えられます。PCと接続してThonnyから動かすときと、直接電源を繋いで単独で動かすときの大きな違いは、PC接続の場合は電源が供給されている状態でそこからプログラムがスタートしますが、単独動作では電源供給開始と同時にプログラムがスタートするという違いがあります。ですので、電源を入れておいてリスタートするということをやってみました。本当はソフト的にリスタートをやりたかったのですが、いろいろ調べてもリスタートの方法がわからず、外にスイッチをつけてハード的にリスタートする方法を試してみました。
nuneno.cocolog-nifty.com
上記の参考サイトによると、RUNピンをGNDに落とすとリセットできるということで、タクトスイッチを繋いでやってみました。
結果は変わらず、突然止まるかどうかは長期間動かさないとわからないので検証できていませんが、カウントが1から始まることと、LINEへメッセージが行かないことが確認できてこの方法は失敗でした。

シリアルポートを繋いで状況を確認する

仕方ないのでもっと状況を把握するためにデバッグ用のシリアル通信文を随所に入れてそれをPCでモニターすることにしました。
通常デバッグするのにPCと繋いでプログラムを動かして、適当なところにPrint文を入れておけばデバッグがやりやすいのですが、今回の場合はPCと繋いでいるときは不具合が出ず、単独動作の時に不具合が出るというので結構厄介です。OLEDに最低限の情報は表示させるようにしているのですが、情報量が少ないですし、書き換えられると残らないので、もう一つあるシリアルポートを活用し、PCとUSBシリアル変換器でつないで情報量を増やして検証してみました。
その結果、LINEに通知を送った時にエラーが返ってきていることが判りました。
問題の箇所は下記の部分で、一定時間経過後カウントがゼロなら(テストプログラムでは5分、本番のプログラムでは24時間)LINEにメッセージを送る部分で、res = ureqests.post(...)の部分がLINEにメッセージを送る部分で、結果をresult=ujson.dumps(res.json())で受けています。結果自体はres.json()に入っているのですが、形式異なるためそのままシリアル出力するとエラーになるので、ujson.dumps()で変換して変数resultで受けています。それをu2.write(result)でシリアル送信しています。

if count==0:      
                res = urequests.post(
                    LINE_url,
                    data = ujson.dumps(data).encode("utf-8"),
                    headers = header
                )
                result = ujson.dumps(res.json())
                oled.fill(0)
                oled.text(result,0,5)
                oled.show()
                res.close()
                u2.write ("send Line Message\n\r")
                u2.write(result+'\n\r')
            else:

エラーになった時のメッセージは
could not read the request body
となっていました。調べると、どうもLINEに渡しているdataの部分が何らかの影響でおかしくなっているらしいということが判りました。やはり起動時に動作が不安定でプログラムが正しく動いていないのではないかという疑いが濃くなりました。

ファームフェアを更新してみる

前回、ESP32での検証の所でも書きましたが、MicroPythonを動かすのにファームウェアが書き込まれています。ファームの不具合があれば動作が不安定になるので、ファームを書き換えてやれば直る可能性があると思ってやってみました。調べてみると、同じ現象ではないのですが、BLINKの動作に不具合があってファームを別のにしたら直ったという人が居ました。
qiita.com
ファームは下記のサイトからダウンロードしました。
micropython.org
以前はv24.0を使っていたのですが、新しいv24.1が出ていたのでこれを使ってみました。
ファイルをダウンロードしたら、ラズパイピコWを一旦PCから切り離し、白いボタンを押しながらPCに再接続すると、ラズパイピコWがドライブとして認識されるので、そのドライブに先ほどダウンロードしたファイルを書き込みます。
結果は、

LINEへの送信ができた

ファームを更新すると今度はLINEに通知がちゃんと行くようになりました。相変わらずカウントは1からのスタートですが。これで、大きく前に進むことができました。

カウントの不具合を修正する

カウントが1から始まるのは、シリアル通信バッファに何らかのデータ残っているためと考え、シリアルポートを開いたときに、バッファをクリアするために何かデータがあればそれを拾ってくる処理を追加しました。

u = UART(1)
if u.any():
    st = uread(1)
u.irq(handler=print_message,trigger=UART.IRQ_RXIDLE)

この処理を追加することでカウントがゼロから始まるようになりました。

ソースコードにミス?

これでめでたく解決。と思ってブログにアップするためにプログラムを再度チェックしたところ、プログラムミスと思われる部分を発見してしまいました。
それは、LINEに渡す変数dataをシリアル受信用のバイト型変数としても使っていてごっちゃになっていた可能性が高いと思われました。
修正したプログラムの全文は下記しますが、冒頭の所でまずUARTに関する設定を行っていて、ここで、

data=b''

とシリアル受信用の変数dataを定義しています。
そして、LINE関係の設定の所で、

data = {
             "to": group_id,
             "messages": [{"type":"text", "text":message}]
             }

とdataをまた定義しなおしています。
これではファームを変えただけでまともにプログラムが動く方が不思議な気がします。
上記のLINE送信時のエラーは当然の結果と思えます。ただ、ファームを変えると何故かちゃんと動いたんですね。
まあ一つ考えられるのは、バイト型のdataとストリング型のdataを別の変数としてPythonが取り扱った場合は動くと思いますが。でも気持ち悪いのでやめた方がいいですね。なので、バイト型の方をudataと別の名前に変えました。

プログラムの全文です。

from machine import UART, Pin, I2C
import micropython
import network
import utime
import urequests
import ssd1306
import gc
import ntptime
import ujson

utime.sleep(1)
utime.sleep(1)

#UART割り込みエラー時の処理に必要
micropython.alloc_emergency_exception_buf(100)
#UART割り込みの有無を知らせるフラグ。Trueで割り込みあり
flag = False
#UARTから受け取るデータ
udata =b''

#UART割り込み処理ルーチン
def print_message(u):
    global flag,udata
    udata = u.read()
    #print(udata)
    flag = True
    return

#LINEにメッセージを送るための変数
LINE_ACCESS_TOKEN=LINEのトークンをここに書く
LINE_url = 'https://api.line.me/v2/bot/message/push'
group_id = LINEのIDをここに書く

#LINEに送るメッセージ
message="24 hours"   #24時間人感センサーの反応がなかった場合
message2 ="battery Low"   #子機の電池電圧が低下した場合
bat = 3.2   #子機の電池電圧 初期値を一応入れてある
#LINE送信用のデータ
data = {
             "to": group_id,
             "messages": [{"type":"text", "text":message}]
             }
data2 = {
             "to": group_id,
             "messages": [{"type":"text", "text":message2}]
             }
header = {
        'Content-Type' : 'application/json',
        'Authorization' : f'Bearer  {LINE_ACCESS_TOKEN}'
        }

#Thingspeak関連の設定
header2={"Content-Type": "application/json"}
THINGSPEAK_URL = "http://api.thingspeak.com/update"
api = ThigsPeakのAPIをここに書く
field = 1    #フィールド1にはシリアル通信の回数
field2 = 2   #フィールド2には子機の電池電圧
server = 'https://api.thingspeak.com/'

#OLED関連の設定
i2c=I2C(0,sda=Pin(16),scl=Pin(17),freq=400000)
oled=ssd1306.SSD1306_I2C(128,32,i2c)

#Wifi関連の設定と接続
SSID = WifiのSSIDをここに書く
PW = WifiのPWをここに書く
oled.fill(0)
oled.text('Hello World', 0, 5)
oled.show()
utime.sleep(2)
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PW)
while wlan.isconnected() == False:
    #print('Connecting to Wi-Fi router')
    oled.text('Connecting', 0, 5)
    oled.show()
    utime.sleep(1)
wlan_status = wlan.ifconfig()
oled.fill(0)
#print('Connected!')
oled.text('Connected!', 0, 5)
oled.show()
#print(f'IP Address: {wlan_status[0]}')
oled.text(f'IP Address: {wlan_status[0]}',0,20)
oled.show()
utime.sleep(2)
#print(f'Netmask: {wlan_status[1]}')
#print(f'Default Gateway: {wlan_status[2]}')
#print(f'Name Server: {wlan_status[3]}')


#num = 1
# NTPサーバーとして"time.cloudflare.com"を指定
ntptime.host = "time.cloudflare.com"

# 時間の同期を試みる
try:
    # NTPサーバーから取得した時刻でPico WのRTCを同期
    ntptime.settime()
except:
    #print("時間の同期に失敗しました。")
    raise

#シリアルポートの設定
u = UART(1)
if u.any():  #始めにバッファをクリアする
    st = uread(1)
#割り込み処理の設定
u.irq(handler=print_message,trigger=UART.IRQ_RXIDLE)

#各種変数
tm = 0     #現在時刻の「時」が入る
otm = -1   #ひとつ前の処理の時の「時」
ttxo = ""  #OLED表示用の文字列
tc = 0     #経過時間保持用の変数
count = 0  #シリアル受信の回数
t = 0      #現在時刻用の変数
x = -1     #シリアル受信した文字列の電池電圧が表示されている位置を示す変数

while True:
    #現在時刻取得
    t = utime.localtime(utime.time() + 9 * 60 * 60)
    #時刻の「時」を入れる
    tm = t[3]
    #OLED表示用の文字列
    ttx = str(t[2])+"/"+str(t[3])+":"+str(t[4])+":"+str(t[5])
    #OLEDに現在時刻、シリアル受信の回数、経過時間を表示
    oled.fill(0)
    oled.text(ttx, 0, 20)
    oled.text('count=%d tc=%d' % (count,tc), 0, 5) 
    oled.show()
    
    if flag == True:	#シリアル通信があった場合
        count = count + 1
        #print(count)
        try:  #受信した文字列から子機の電圧が入っている場所を探す
            datastr = udata.decode('ascii')
            x = datastr.find('ba=')
        except:  #正しく文字列が受信できていなかったらエラーを表示
            oled.text('decode error',0 ,5)
            oled.show()
            #print('decode error')
        
        if (x!=-1):  #場所が見つかったら
            try:   #文字列(mV単位)をVの浮動小数点に直す
                bat = float(datastr[x+3:x+7])/1000
                oled.text(str(bat),0,5)
                oled.show()
                #print(bat)
            except: #変換エラーの場合
                oled.text('float convert error',0,5)
                oled.show()
                #print('float convert error')
       
        flag = False
        
    if (otm != tm):  #「時」が一つ進んだ場合
        otm = tm
        tc = tc + 1
        oled.fill(0)
        #Thingspeakにカウント数と電池電圧をUPする
        #print(tm)
        #print(tc)
        gc.collect() #これをしないとメモリーが足りなくなりエラーになる
        url = f"{server}/update?api_key={api}&field{field}={count}&field{field2}={bat}"
        try:   
            response = urequests.post(url)
            #print('mem_free = %d' % gc.mem_free())
            response.close()
            #残りメモリーをOLEに表示
            oled.text('mem_free=%d' % gc.mem_free(), 0, 5)
            oled.show()
        except:  #ThingsPeakへのPOSTがエラーになった場合
            #print("error")
            ttxo = "post error"
            oled.text(ttx, 0, 20)
            oled.text(ttxo, 0, 5)
            oled.show()

        if tc == 24:   #24時間経過したとき
            tc = 0     
            if count==0: #カウントがゼロだったらLINEにメッセージを送る      
                res = urequests.post(
                    LINE_url,
                    data = ujson.dumps(data).encode("utf-8"),
                    headers = header
                )
                result = ujson.dumps(res.json())
                oled.fill(0)
                oled.text(result,0,5)
                oled.show()
                res.close()
            #print ("send Line Message")        
            else:
                count=0
            if bat<2.9:   #子機の電池電圧が2.9V以下になったらLINEにメッセージを送る
                res = urequests.post(
                    LINE_url,
                    data = ujson.dumps(data2).encode("utf-8"),
                    headers = header
                )
            #print (res.json())
                res.close()
                
    utime.sleep(1)
||<