2024年3月19日火曜日

python+matplotlibでCOM port受信データを動的plot 【完結編】

COM port受信データを動的plotしたい.今日もぱいぱい.

先日の投稿の続きの完結編です.自分がやりたい機能を網羅できましたのでupするです.

やりたいことリスト:
 ・arduinoを一種のロガーとしてあらかじめ作っておく
 ・arduinoはUSB COMに向けてADCのascii数値を毎秒打ち続ける
 ・PCのアプリでリアルタイムのグラフをplotする ←本稿の目的
 ・グラフを自動scalingする
 ・ADC値をplotするだけでなく、平均値もplotする
 ・平均値の最初の1発は最初のADC値にする
 ・ADC値をuserが好むscaleに換算して表示させることもできる
 ・userはグラフをクリアできる
 ・text fileにログを保存する →後でexcelで開ける
 ・誰でも動かせる →python+matplotlib+powershell
 ・グラフアプリをどのCOM portに接続するかを選べる
これだけ出来ればだいたいやりつくしました.思い残すことは無い...

netには断片情報はたくさん流通しているけれど、技術者が実際にロガーを動かすときには上の箇条書きを全部網羅しないと現実には使えません.以下では上の機能を網羅したグラフアプリをお届けします.細かいところは読者なりにカスタマイズしてください.

なお、読者に損害を与えても知らんです.

ーーーー
以下でcodeの説明をします.

pythonのcodeをこちらにリンクしておきます.

動かすまで
PCのwin10のpython3です.

pythonを最新にした他、少なくとも以下のmoduleを追加しました.他にも何か入れたかもしれませんが忘れました.
 pip install matplotlib
 pip install animation
 pip install pyserial
 pip install numpy

codeをどこか適当なフォルダに入れて、powershellでそのフォルダにcdし、
 python COM_logger_graph.py
と打てば起動します.pathはよしなに.

powershell以外の仮想環境を試したけどCOMが動きませんでした.Anacondaのjupyterとspyderでダメでした.


グラフアプリの動作
起動すると最初に接続先のCOMを訊かれます.
COMに回答するとグラフ窓が開きます.
デフォルトでは縦軸のscaleは±512です.これはArduinoのADCの分解能1024に由来します.

しかしADC値だけ見ていても意味が薄いです.物理量に換算するべきです.①②でそれを指定します.例えば「ADC値=100のときに0.33A」などという裏事情がある時には、
 ①=100
 ②=0.33
と入力すると、画面のY軸がそれに合わせてscalingされます.

scaling前のplotが残るのが邪魔な場合は、③clear graphを押すとまっさらから再描画します.

ADC値のplotに戻したい場合は、④No conversionを押します.

グラフ画面の使い方は以上です.


ログファイルはADCdata.txtという名前で同じフォルダに生成されます.
中身は、こんな感じです.数値は2つ並び、左列はADC値、右列は換算値 です.③clear graphを押してもログはクリアされませんでappendされ続けます.①②③④をいじった時に"changed scaling"の文字がログに残るのが目印です.
 83.0 0.000162109375
 93.0 0.000181640625
 99.0 0.000193359375
 changed scaling
 124.0 0.0002421875
 123.0 0.000240234375
 80.0 0.00015625
 156.0 0.0003046875
 98.0 0.00019140625


グラフアプリのcode
けっこう長いけど上から見て参ります.

いきなりglobal変数を4つ使います.classに閉じ込めた方がよかったな.
上2つは物理量換算するための、TextBox①②で人間が入力した数です.
3つ目はADC平均値を入れる変数.
4つ目はCOM3とかの文字列.
ADCvalue=512 # for conversion
Fvalue=512 # for conversion
FLTiir=99999 # LPF of ADC
COM = "" # COM port string

どのCOMを使うかを問うdialogを表示して、COM openするサブルーチン.
def COMopen():

select COM portと表示した小窓を開く.libの操作に統一感が無いなぁ.
 root = tk.Tk()
 label=tk.Label(root,text="select COM port")
 label.pack()

プルダウンリストのcallback関数.COMに"COM3"などの文字列が入る.最後にdestroyするのが重要です.これで小窓を消すことで、blockしてた処理が先に進みます.
 def setCOM(event):
  global COM
  COM = combo.get()
  root.destroy()
余談ですが、気持ちとしてはreturn COMとかやりたいけれど、これってcallback関数なので返り値を受ける事が出来ず、わざわざglobal COMに入れてます.なんだかなー

プルダウンリストの操作.libの操作に統一感が無いなぁ.
2行目でプルダウンリストに選択肢を登録しています.
3行目はコメントアウトですが、デフォルトで3番目を選択した状態にできます.
4行目はcallback関数の登録.
5行目で表示.
6行目はここから先をblockします.先へ進ませるにはdestroy()すればよい.
 combo = ttk.Combobox(root,state="readonly",text="select COM port")
 combo["values"]=("COM0","COM1","COM2","COM3","COM4",....."COM8","COM9")
 #combo.current(3)
 combo.bind( " <<ComboboxSelected>> " , setCOM )
 combo.pack()
 root.mainloop()

ここから下は人間がCOMを選択した後の処理です.COM openして、portを返します.
 port = serial.Serial(COM, baudrate=115200, parity=serial.PARITY_NONE)
 return port
以上でCOMopen()は終了.


以下はメインルーチン.けっこう長い.
def main():

上述のCOMopen()を行い、ログファイルもopenします.
 port = COMopen()
 f = open('ADCdata.txt', mode='w')

グラフ窓を作成.
figは窓を意味する.axはグラフ領域とかTextBoxとかボタンを指す.
1つのfig窓に、5個のaxを生成しています.第一引数の5が縦並びで5個のaxを生成する意味.[10,1,1,1,1]は5個のaxの縦サイズを10:1:1:1:1で描くの意味.
 fig, ax = plt.subplots(5, 1, figsize=(7,7), gridspec_kw=dict(width_ratios=[1], height_ratios=[10, 1,1,1,1]))

せっかく10:1:1:1:1で描いたのにそれはご破算にして、もっと詳細に位置決めしてやります.5個のaxを[0]~[4]でindexする.
 ax[0].set_position([0.2, 0.35,          0.75, 0.6 ]) # left,low,width,hieght グラフ
 ax[1].set_position([0.5, 0.05*3+0.01*4,  0.4, 0.05]) TextBox
 ax[2].set_position([0.5, 0.05*2+0.01*3,  0.4, 0.05]) TextBox
 ax[3].set_position([0.5, 0.05+0.01*2,    0.4, 0.05])  ボタン
 ax[4].set_position([0.5, 0.01  ,         0.4, 0.05])    ボタン

動的グラフ書きを起動します.matplotlib.animationの関数です.
figはグラフ窓を指しています.
updateはグラフ描きcallback関数を指定します.
60*60*24*3は3日間の秒数の意味で、謂わば「総セル枚数」のこと.この関数は無限loopじゃないんですね.
 anime.FuncAnimation(fig, update, range(60*60*24*3), repeat=False)


update()はグラフ描き関数です.
引数のiは使ってないけど書かないとerrになります.
COMから1行読んで、spaceで分割します.COMの1行は「ADC(sec)= 492\n」みたいな書式にしてあります.
 def update(i):
  r = port.readline()
  s = r.split()

COM文字列からADC値(0~1023)をfloatで切り出し.±512に縦ずらし.
  if len(s)==2 and s[0].decode('utf8')=='ADC(sec)=' :
   adc = float(s[1]) - 512

ADC値を物理量に換算.デフォではadc/512*512なのでADC値のままplotされます.
   field = adc/ADCvalue*Fvalue

物理量をIIR LPFで平均化する.99999というナンセンス値だったら最初の一発目なので物理量をそのまま採用.そうでなければLPFする.LPF時定数は適当に定めました.
   if FLTiir==99999 :
    FLTiir = field
   else :
    FLTiir = 0.05*field + 0.95*FLTiir

powershellに表示します.
   print(adc," ",field)

logファイルに出力します.
   f.write(str(adc)+' '+str(field)+'\n')

グラフのy軸リストに物理量をappend.リストy2にはLPF出力をappend.
   y.append(field)
   y2.append(FLTiir)

ax[0]はグラフ領域.最初にクリアして、yをplotして、y2をplotする.
   ax[0].clear()
   ax[0].plot(y)
   ax[0].plot(y2)

グラフのy軸をfixにしたければset_ylimすれば良いが、ここではauto scalingしたいのでコメントアウトしてある.
   #ax[0].set_ylim([0, 1024])

グリッド線を描く.お任せなので便利でいいですね.
   ax[0].grid()
以上でグラフ描画関数update()はおしまいです.


ここから下は窓のTextBox①②とボタン③④の処理です.

1行目はax[1]の区画をTextBoxとし、ADC valueと表示させる.
2行目は現在の値をセット.
3行目は人力で入力されたらADCval_eventというcallbackを呼び出す.
 ADCval_box = TextBox(ax[1], 'ADC value', textalignment="center")
 ADCval_box.set_val(ADCvalue)
 ADCval_box.on_submit(ADCval_event)

それでもってcallback関数がこれ.引数vにtextboxに入力された値が入ってるので、それを採用し、LPF変数にはアリエナイ99999を入れておく.以下略.
 def ADCval_event(v):
  ADCvalue = float(v)
  FLTiir=99999


最後にclear graph buttonの処理を説明します.
ax[3]の区画をボタンとし、clear graphと表示させます.hovercolorはマウスをボタンに合わせたら色が変わります.
ボタンが押されたらbtn1_clickというcallback関数が呼ばれます.
 btn1 = wg.Button(ax[3], 'clear graph', color='#f8e58c', hovercolor='#38b48b')
 btn1.on_clicked(btn1_click)

それでこれがcallback関数.y軸リストをクリアして、LPF変数にはアリエナイ99999を入れておく.以下略.
 def btn1_click(event):
  y.clear()
  y2.clear()
  FLTiir=99999


最後に描画開始させます.
 plt.show()


かしこ

2 件のコメント:

  1. 平坂さんは本当,ハードウェアのフルスタックエンジニアですね。A fully armored engineerとでも呼んだほうがいいかも。

    返信削除
    返信
    1. アウトソーシングに背を向けて幾年月.
      深夜残業を繰り返して幾年月.

      削除