Python独習!

習得したPython知識をペイフォワード

Pythonでシリアル通信!Arduinoに文字列を送って、Arduinoから文字列を受け取る!

2021/01/24 送受信の所要時間に関する記述を追加
シリアル通信でLチカしたプログラムをアップデート。Arduinoに特定の文字列を送ると、それに対応した返答を返すというもの。
RS232Cを備える測定器をリモートコントロールするための練習プログラム。

greenhornprofessional.hatenablog.com

結果

Pyhtonから送りたい文字列を標準入力で入力すると、Arduinoから意図したとおりの返答を受け取れている。
送受信は30~40msで完了している。この数字が妥当なのか?などの深堀はまた今度。
また、送信する文字列の末尾にセミコロンをつけていない場合は時間が増える。これはArduinoSerial.readStringUnitl()セミコロンを待つようにしているためで、これのタイムアウト(恐らく1sec)が影響している。

<= abc;
計測開始
送信完了:0.008sec
=> Input is abc
=> Return is ABC
受信完了:0.037sec

<= abc
計測開始
送信完了:0.010sec
=> Input is abc
=> Return is ABC
受信完了:1.020sec

<= ddd;
計測開始
送信完了:0.009sec
=> Input is ddd
=> N/A
受信完了:0.033sec

プログラム

Arduino
void setup(){
  Serial.begin(115200);
}

void loop(){
  if(Serial.available()>0){
    String input = Serial.readStringUntil(';');
    Serial.print("Input is "); Serial.println(input);
      if(strcmp(input.c_str(), "abc") == 0){
        Serial.println("Return is ABC");
      }else if(strcmp(input.c_str(), "def") == 0){
        Serial.println("Return is DEF");
      }else{
        Serial.println("N/A");
      }
  }
}
Python
#SimpleSerial_003.py

import serial
from time import sleep
import time

def decoder(byte):
    str_array = []
    str_array = byte.decode().rstrip('\r\n').split('\r\n')
    for i in str_array:
        print("=>",i)
    
def main():
    ser = serial.Serial('COM3', 115200, timeout=0.1)
    sleep(2)
    
    command = input()
    print("<=", command)

    t0 = time.time()
    print("計測開始")
    
    ser.write(bytes(command, encoding='ascii'))
    ser.flush()

    t1 = time.time()
    print("送信完了:{:.3f}sec".format(t1 - t0))

    while True:
        if ser.in_waiting > 0:
            data = ser.read_all()
            break
        
    decoder(data)

    t2 = time.time()
    print("受信完了:{:.3f}sec".format(t2 - t1))
    
    ser.close()

if __name__ == '__main__':
    main()

コメント:今回のプログラムではser.flush()は時間に寄与せず。

Pythonでシリアル通信してArduinoをLチカさせる

とある測定器をRS232Cでリモートコントロールしたくて、Pythonのシリアル通信の仕方を学ぶ。まずは簡単なところからということで、Pythonから送られてきた数字(トリガー)をArduino側で識別してそれぞれに対してLチカの挙動を変える、というプログラムを作ってみた。

結果

0 : 消灯、1 : 点灯、2 : 5回点滅、その他 : 10回点滅、という条件分岐がちゃんとできている。
以下のPythonプログラムは"2"の動作のみ。

プログラム

Arduino
void setup(){
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
}

void loop(){
  void flicker(int, int);
  int input = Serial.read();
  if(input != -1){
  Serial.println(input);
    switch(input){
      case '0':
        digitalWrite(LED_BUILTIN, LOW);
        break;
      case '1':
        digitalWrite(LED_BUILTIN, HIGH);
        break;
      case '2':
        flicker(5, 500);
        break;
      default:
        flicker(10, 100);
     }
  while(Serial.available())Serial.read();
  }
}

void flicker(int n, int intval){
  for(int i=0; i<n; i++){
    digitalWrite(LED_BUILTIN, HIGH);
    delay(intval);
    digitalWrite(LED_BUILTIN, LOW);
    delay(intval);
  }
}
Python
#SimpleSerial_001.py

import serial
from time import sleep 

ser = serial.Serial('COM3', 115200, timeout=0.1)
sleep(2)
ser.write(bytes('2', encoding='ascii'))
ser.close()

Pythonでフリー公開されているDLLを使ってみる

ctypesを使ってDLLの呼び出し方を学ぶ。今回はフリーで公開されているDLLを使ってみる。前回よりも難しかった。
greenhornprofessional.hatenablog.com


使用させてもらったDLLは以下。文字列として書かれた式と変数の値を与えると式通りに計算してくる、というもの。
文字列よりの計算の詳細情報 : Vector ソフトを探す!

結果

式 x+y+zと、変数 x=10, y=2, z=0.1 を与えると、12.1が返ってきた。ちゃんと動いている。
今回はたまたま出来たというのが正直なところ。C/C++を勉強しないとこれよりも複雑なDLLは使えない。
プロトタイプに標準データ型以外の引数が書かれていると絶望的になる…

プログラム

# 31_CallCalcDll_001.py
# python 3.8.1
# coding: utf-8

from ctypes import *

p_ans = POINTER(c_double)
p_siki = POINTER(c_char)

lib = cdll.LoadLibrary('CALC11')
lib.Num_Form_Calc.argtypes = (p_siki, c_double, c_double, c_double, p_ans)
lib.Num_Form_Calc.restype = c_int

siki = b"x+y+z"
x = 10
y = 2
z = 0.1
ans = c_double()
i = c_int()

i = lib.Num_Form_Calc(siki, x, y, z, ans)

print(i)
print(ans)
print(ans.value)
0
c_double(12.1)
12.1

Pythonでダミーファイルを大量生産する

ソフトウェアの動作確認のために大量の画像ファイルが必要になった。中身はどうでもよく、とにかく数が必要。
1つの元画像をコピーして連番をつける、というプログラムをちゃちゃっと作った。

プログラム

# 32_CopyAndRename_001.py
# python 3.8.1
# coding: utf-8

import os
import shutil

num = 10            #Specify how many copies do you want.
_dir =  'C:\Test'   #Work directory
src = 'lenna.jpg'   #Original file name
src_path = os.path.join(_dir, src)              #Make the path like C:\aaa\bbb\ccc.jpg

if os.path.isfile(src_path):                    #Check if the original file exists.
    for i in range(1, num+1):
        copy = str(i).zfill(3) + '.jpg'         #Define sequential name like 001.jpg, 002.jpg, ...
        copy_path = os.path.join(_dir, copy)    #Make a path for copy like C:\aaa\bbb\001.jpg.
        shutil.copyfile(src_path, copy_path)    #Execute copy.
    print("Complete!")
else:
    print("No file found.")

Pythonでdllを使う Windows API - MessageBox

マシンビジョンカメラをコントロールするソフトが作りたい。でもPython向けのAPIが公開されていない。調べてみたらctypesでdllを使うことができるらしい。
とりあえず、WindowsのMessage Boxの呼び出し方を紹介してくれているサイトがあったので、参考にさせてもらった。

結果

以下のメッセージボックスを呼び出すことができた。メッセージボックスを表示させるだけならTkinterよりもお手軽。
f:id:greenhornprofessional:20200719193538p:plain

プログラム

# 30_WinMessageBox_001.py
# python 3.8.1
# coding: utf-8

import ctypes

mbox = ctypes.windll.user32
i = mbox.MessageBoxW(
    0,
    "[テスト] 予期せぬエラーが発生しました",
    "Error Message",
    0x00000002 | 0x00000010)

if i == 3:
    print("中止を選択しました")
elif i == 4:
    print("再試行を選択しました")
else:    #i == 5
    print("無視を選択しました")

Pythonでpipが対応しているWheelファイルを調べる

2021/01/13 更新

ネットワークにつながっていないパソコンにモジュールを追加したい場合、そのモジュールのWheelファイル(.whl)を使うとことでインストールすることができる。Wheelファイルを適当なディレクトリに置いて、pipで指定してあげればよい。ただし、自分のpython環境にマッチしたWheelをもってくる必要がある。
どうやってそれを調べるか?ググればたくさん出てくるが、それ通りにはできなかったので忘備録を残しておく。Python3.8 pip 20.1 環境における対応cpの調べ方の記事を見つけられなかったので苦労した

cpとはCPython versionの頭2文字のこと。Python = CPythonと思っていてよい。
What does version name 'cp27' or 'cp35' mean in Python? - Stack Overflow

要件

Pythonのバージョンを確認する

例えば、v3.8.1だったら『cp38』に対応したWheelファイルが必要になる。v3.7.* であれば『cp37』となる。

pipが対応しているcpを確認する

cp38に対応したWheelファイルを見つけても、pipがcp38に対応していないとインストールできない。
以下の.pyを実行すると対応cpがわかる。

#python 3.8.1
#pip 20.1.1
from setuptools import pep425tags
print(pep425tags.get_supported())
[('cp38', 'cp38m', 'win32'), ('cp38', 'none', 'win32'), ('py3', 'none', 'win32'), ('cp38', 'none', 'any'), ('cp3', 'none', 'any'), ('py38', 'none', 'any'), ('py3', 'none', 'any'), ('py37', 'none', 'any'), ('py36', 'none', 'any'), ('py35', 'none', 'any'), ('py34', 'none', 'any'), ('py33', 'none', 'any'), ('py32', 'none', 'any'), ('py31', 'none', 'any'), ('py30', 'none', 'any')]

Pythonでグラフ画像から数値を読み取ってエクセルに出力する

シーンとしては、例えば、製品Aと製品Bの性能を比較するときに特性図(グラフ)を参照することがある。カタログやWebにグラフは掲載されているが、画像になっているので重ね合わせて比較することができず、なんとなくAの方が優れてるかな?なんてあいまいな感じに終わってしまう。メーカーに問い合わせても数値を提供してくれることは稀。まぁそりゃそうだろう。
ということで、画像になっているグラフから数値を読み取って、それをエクセルに出力するプログラムを作った。

結果

参考データとしては十分に使えるレベルで数値化できたと思う。画像処理の膨張縮小、線の検出で位置ズレが起きているはずなのであくまで参考データ。
左が入力画像、右が出力結果。数値はエクセルに出力するがグラフ化は手作業で。
f:id:greenhornprofessional:20200603214010p:plain

注意事項としては、

  • 第一象限のグラフしか対応していない。(マイナスを含むデータは非対応)
  • 対数グラフは対応していない。
  • 入力するグラフ画像は補助線よりもデータの線が太い必要がある。画像処理で消せなくなる。
  • 入力するグラフ画像の淵はペイントなどで消す必要あり。上記の左画像の元データは以下。

 f:id:greenhornprofessional:20200603214749j:plain

プログラム

相変わらず、センスのなさに歯がゆい思いがする…まぁ動くのでよしとするが。

# 29_ExtractChart_001.py
# python 3.8.1
# opencv-python 4.1.2.30
# coding: utf-8
#
import cv2
import numpy as np
import datetime
import openpyxl

#===================#
# Define parameters #
#===================#
image = "f.png"
k = 2
th = 100
y_max = 1
x_min = 350
x_max = 750

#=================#
# Define function #
#=================#
# Function for extracting XY data from an imaged chart.
def get_profile():
    ## Add closing and threshold to erase auxiliary lines on the chart.
    img = cv2.imread(image, 0)
    kernel = np.ones((k, k), np.uint8)
    ret,img = cv2.threshold(img, th, 255, cv2.THRESH_BINARY)
    img = cv2.dilate(img, kernel,iterations = 1)
    img = cv2.erode(img, kernel,iterations = 1)
#    img = cv2.dilate(img, kernel,iterations = 10)
#    img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
#    img = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
    cv2.imshow("output", img)

    ## Get num of pixels of img. h:height  w:width
    h, w = img.shape
    
    ## Array for XY data. [X, Y].    
    profile = []
    ## print(type(profie)) -> <class 'list'> Not nparray!

    ## Get each XY-coordinate of each points on the line of the chart. 
    for i in range(w):
        line = img[:,i]
        edge = [j for j in range(h-1) if line[j] != line[j+1]]      ##Seach two inflection points (W to B and B to W) on a vertical line.
        if len(edge) < 1:
            val = 0
        elif len(edge) == 1:
            if edge[0] > h/2:
                val = 0
            else:
                val = h
        else:
            val = h - sum(edge)/2       ## Reverse Y-coordinate(top to bottom -> bottom to top). 
        point = [i+1, val]              ## XY data [X, Y]
        profile.append(point)

    ## Do unit conversion on X and Y coordinate.
    y = max(profile, key= lambda p:p[1])[1]     ##Get maximum value of Y data.
    for q in range(w):
        profile[q][0] = profile[q][0] * (x_max - x_min) / w + x_min
        profile[q][1] = profile[q][1] * y_max / y

    return profile

# Export 2Darray to xlsx format.           
def export_xlsx(arry):
    wb = openpyxl.Workbook()
    ws = wb.create_sheet(index= 0, title = "Line profile")
    ws = wb["Line profile"]             ## I don't know how this works.
    wb.active = wb.sheetnames.index("Line profile")

    row = 1
    for i in arry:
        celA = "A" + str(row)
        celB = "B" + str(row)
        ws[celA] = i[0]
        ws[celB] = i[1]
        row += 1
    try:
        now = datetime.datetime.now()
        wb.save('LineProfile_{0:%Y%m%d%H%M%S}.xlsx'.format(now))
        print("Save completed")
    except:
        print("Save failed!")
  
#======#
# Main #
#======#
if __name__ == "__main__":
    export_xlsx(get_profile())
/* -----codeの行番号----- */