プログラムから実際のハードウェアを制御する方法の一つとして、電子計算機の初期からいまも使われている シリアル通信があります。今回はこのシリアル通信を使って実際のハードウェアを操作するプログラムを Pythonを使って作成してみましょう。
今回は、EPICS講習会などで使われたArduinoをベースにした簡単な装置を、Pythonのプログラムから制御する方法を見ていきます。 この装置を使うことで、計算機からハードウェアを制御するために バイナリ、アナログのデータをやりとるする方法の基礎を経験することができます。
電子計算機で装置を制御する際には、計算機と装置とのデータを交換する方法としてパラレル方式とシリアル方式があります。パラレル方式では複数のビットデータを、そのビット数に応じた信号線を使って同時にそのデータを送受信する方法です。一方、シリアル方式では一本の通信線に複数のビットデータを時間的に分割して順次送信します。
EIA-232(RS-232)はUSBが普及する以前に広くつかれていたシリアル通信規格です。 DB(D-Sub)-25pinあるいはDB-9(DE-09)pinのケーブルで機器(DTE:data terminal equipment)と通信装置(DCE:data communication equipment, MODEM:Modulator-Demodulator)を接続するために使われていました。 TxDおよびRxDの信号線の他に、DTR(Data Terminal Ready), DCD(Data Carrier Detect), DSR(Data Set Ready),RTS/RTR(Request To Send/Ready to Receive), CTS(Clear To Send) といった通信制御のための信号線を持っています。
注:DB-9:D-sub コネクターの9pin版。正しくはDE-9らしい。(D-subにはDA-DEの5タイプと3種類のDensityによる分類がある)。 アストンマーチン DB-9とは無関係
初期のPCでは、EIA-232の信号(制御信号を含む)をサポートするために、DB-9コネクタが標準的に使われていました。制御信号を省くことで3芯のケーブル(Tx/Rx/GND)でも通信は可能となるので、その他のコネクタ(RJ-45など)が使われる場合もありました。
シリアル接続用ケーブルの一例として、USB-Serial変換ケーブルの一例(写真左)を示します。ケーブルの一端にUSB- Aコネクタ、反対の一端にはDB-9コネクタがUSB-Serial変換機能を持つ中央部を経由して接続されています。 写真右は同様のUSB-GPIB変換ケーブルです。GP-IBのコネクタはアンフェノール社が設計した24ピンのマイクロリボンコネクタになっています。GP-IBはパラレル接続ですので、EIA-232に比べ信号線の数が増えています。
パラレル通信の一つの例としてIEE-488(GP-IB)規格があります。
IEEE-488規格はGPI-IB/HP-IBに起源をもつ、機器制御の国際規格です。現在のIEEE-488規格はIEEE-488.1, IEEE-48.2の二つの規格に分かれています。IEEE-488.1はGP-IBの電気的な規格を定めています。一方、IEEE-488.2はこの規格に従う機器の制御で使われるコマンドやデータのフォーマット、標準的なコマンドについて規定しています。IEEE-488.2は関連するSCPI ともに計算機と計測機器の間の通信のためのコマンドやデータのフォーマット、標準コマンドな度を規定しており、VXI11, HiSLIP, USBTMCなどのシリアル通信を使った方式でも採用されています。
Pythonでシリアル通信を取り扱うための基本モジュールはPySerial
モジュールです。標準配布のモジュールではありませんので、PyPIからインストールします。
お使いの環境によっては、すでにインストール済みあるいはその環境のパッケージ管理システム(apt, yum/dnfなど)をつかってインストールすることも可能でしょう。
pyserial
モジュールのインストール¶PIPを使う方法
python3 -m pip install pyserial
yum/dnf(Redhat系Linux, RH, Centなど)
yum install pyserial
pyserial
モジュールのインポート¶配布パッケージの名前はpyserial
ですが、python モジュールとしての名前はserial
となっています。したがって、
from serial import *
あるいは
from serial import Serial
などで、必要なクラスをインポートします。
なお、このノートで使用しているpyserial
のバージョンは次のようになっています。異なるバージョンのpyserial
では一部のプログラムで変更が必要なものがあります。
import serial
print(serial.__version__)
3.5
pyserial
のサブモジュール¶pysieral
には、次のようなサブモジュールが用意されています。
rs485
: RS485(multidropのシリアル通信)rfc2217
: "Telnet Com Port Control" TCP-serialコンバーターによるシリアル通信comports()
などシリアルポートの検索、詳細情報の入手などいくつかのツールここで使う装置は、
この試験装置には、"gainermodoki32u"プログラム(Arduinoではスケッチと呼ばれます)が予めダウンロードされています。このプログラムが起動しているAruduinoは一文字のコマンドと二つまでの引数の組み合わせで、接続されている素子を操作することができます。
コマンド文字 | 引数 | 意味 |
---|---|---|
H | pin番号(D) | ビット出力を1にする |
L | pin番号(D) | ビット出力を0にする |
R | pin番号(D) | ビット入力を読み込む |
a | pin番号(D)+出力値 | PWM出力値設定 |
S | pin番号(A) | アナログ入力値を読み込む |
M or ? | NA | バージョン番号 |
コマンドで使われるピン番号と接続されている素子の関係は次の様になっています。
表:コマンド引数のピン番号と素子の端末との関係
pin | signal | pin | signal |
---|---|---|---|
D3(PWM/AO) | 3-LED(R) | D2(DO) | Buzzer |
D5(PWM/AO) | 3-LED(G) | D7(DI) | Button(SW) |
D6(PWM/AO) | 3-LED(B) | D8/D12(DI) | 傾き(Tilt)スイッチ |
D9(PWM/AO) | LED(G) | A0(AI) | Cds |
D10(PWM/AO) | LED(R) | A1(AI) | Thermistor |
D13(DO) | On boad LED | A5(AI) | Vdd |
少し前までのPCには、Serial Port(EIA-232(RS-232))のDSUB-9pinコネクタを持っているものも少なくありませんでした。しかし、2022年現在のPCではそういったPCは滅多にお目にかかりません。したがって、最近のPCをシリアル接続しか受け付けない装置と接続しようとすると、USB-シリアルコンバータやネットワーク経由のターミナルサーバーをつかうことになります。
USB-CDC(Communication Device Class)はUSB-Serialコンバーター/USB-UART(Universal Asynchronous Receiver/Transmitter)経由でシリアル通信を行う際に使われる規格です。
今回使用しているArduino UNOにはメインのCPU(ATMEGA328P-PU)の他に、main CPUとUSBportの間にあって、USB-Serial コンバータの役割を果たしている副CPU(ATMEGA16U2-MU)が搭載されています。これによって、Arduino UNOは別途コンバータを用意することなく、USB-CDCデバイスとして働きます。(Arduinoのバージョンによっては、別途USB-Serial コンバーターを用意する必要があります。)
USB-CDCで接続された装置は、OSからシリアル通信装置(tty,cu:calling unit)として認識され、デバイスファイルが/dev
以下に登録されます。登録されるデバイスファイル名はOSに依存します。
macos
の場合は次のようになります。接続されたUSBポートを示す番号がデバイスファイル名に含まれています。% ls -l /dev/tty*usb*
crw-rw-rw- 1 root wheel 0x9000002 3 1 09:51 /dev/tty.usbmodem1101
/dev/serial/by-id
を調べることで、接続された装置がどのデバイスファイルに結びついているかを確認できます。$ ls -l /dev/serial/by-id
合計 0
lrwxrwxrwx. 1 root root 13 Feb 11 07:41 usb-Arduino_LLC_Arduino_Micro-if00 -> ../../ttyACM2
lrwxrwxrwx. 1 root root 13 Feb 18 09:45 usb-Arduino__www.arduino.cc__0043_64936333037351904041-if00 -> ../../ttyACM4
lrwxrwxrwx. 1 root root 13 Feb 11 07:41 usb-Arduino__www.arduino.cc__0043_9314036423335140F032-if00 -> ../../ttyACM3
glob
モジュールや、os
モジュール、fnmatch
モジュール、re
モジュールを使って、 Arduino装置がOSに認識されているかを
確認してみましょう。
glob.glob()
は引数に与えられたパス名のパターンに合致するファイルのパス名のリストを返します。
import glob
glob.glob("/dev/tty*usb*") # Linuxでは、"/dev/ttyACM*"などとしてみましょう。
['/dev/tty.usbmodem11101']
glob.glob()
関数の返す値はデバイスファイル名のリストなので、最初の要素[0]
だけを取り出します。
ttydev=glob.glob("/dev/tty*usb*")[0]
ttydev
'/dev/tty.usbmodem11101'
os
モジュールのlistdir
関数は引数に指定されたパスに在る ファイル名のリスト を返します。
その中から、fnmatch
モジュールのfnmatch
関数を使ってパターンに合致するファイル名だけを選び出します。
import os, fnmatch
[file for file in os.listdir("/dev/") if fnmatch.fnmatch(file, 'tty*usb*')] # unix pattern
['tty.usbmodem11101']
正規表現(regular expression)を使ってファイル名のリストから選択することもできますね。 (パターン "tty*usb*" と正規表現 "tty.*usb.*" の違いに注意してください。)
import os, re
[file for file in os.listdir("/dev/") if re.match('tty.*usb.*',file)] # regular expression
['tty.usbmodem11101']
Windowsではシリアル通信のデバイスは"COM2"
などの名前で指定します。この名前と実際に接続されている装置の対応については、Windowsのマニュアルをご覧ください。
serial.tools.list_ports
モジュール¶pyserial
モジュールで操作可能なポートのリストを入手するには、
serial.tools.list_pots
モジュールのgrep()
やcomports()
を使うことも可能です。
このモジュールを使うことで、より詳細なポートの情報を入手可能です。(platformによる, macosxではポートの詳細情報が得られない。→ pyserial 3.5では修正済)Linuxでの使用例を示します。
shellで直接確認することもできます。
bash
$ python3 -m serial.tools.list_ports_osx
/dev/cu.AUKEYBR-C1: n/a [n/a]
/dev/cu.Bluetooth-Incoming-Port: n/a [n/a]
/dev/cu.usbmodem11101: Arduino Uno [USB VID:PID=2341:0001 SER=6493534363335151A102 LOCATION=1-1.1]
/dev/cu.wlan-debug: n/a [n/a]
$ python3 -m serial.tools.list_ports_linux
/dev/ttyACM0: USB-ADC AD7793 CQ Board [USB VID:PID=04D8:0FBA LOCATION=3-1.3.1:1.0]
/dev/ttyACM1: USB-ADC AD7793 CQ Board [USB VID:PID=04D8:0FBA LOCATION=3-1.3.3:1.0]
/dev/ttyACM2: Arduino Micro [USB VID:PID=2341:8037 LOCATION=3-4.3:1.0]
/dev/ttyACM3: ttyACM3 [USB VID:PID=2341:0043 SER=9314036423335140F032 LOCATION=3-1.3.4:1.0]
(お願い:serial.tools.list_ports_windows
をwindows PC上で実行した結果をお知らせください。)
pythonプログラム中のserial.tools.list_ports.comports()
使用例:
import serial.tools.list_ports
for port, desc, hwid in serial.tools.list_ports.comports():
print(port, desc, hwid)
print("\n serial.tools.list_ports.grep examples\n")
# serial.tools.list_ports.grep を使って、デバイス名、 description あるいは hwid に指定文字列を含むもの
for port, desc, hwid in serial.tools.list_ports.grep("usb"):
print (port, desc, hwid)
for port, desc, hwid in serial.tools.list_ports.grep("Arduino"):
print (port, desc, hwid)
for port, desc, hwid in serial.tools.list_ports.grep("2341"):
print (port, desc, hwid)
/dev/cu.wlan-debug n/a n/a /dev/cu.Bluetooth-Incoming-Port n/a n/a /dev/cu.AUKEYBR-C1 n/a n/a /dev/cu.usbmodem11101 Arduino Uno USB VID:PID=2341:0001 SER=6493534363335151A102 LOCATION=0-1.1.1 serial.tools.list_ports.grep examples /dev/cu.usbmodem11101 Arduino Uno USB VID:PID=2341:0001 SER=6493534363335151A102 LOCATION=0-1.1.1 /dev/cu.usbmodem11101 Arduino Uno USB VID:PID=2341:0001 SER=6493534363335151A102 LOCATION=0-1.1.1 /dev/cu.usbmodem11101 Arduino Uno USB VID:PID=2341:0001 SER=6493534363335151A102 LOCATION=0-1.1.1
ArduinoがOSに認識されていることが確認できましたので、 pythonプログラムを使って、ボードの動作を確認してみましょう。 まずは、プログラムをデバイスにシリアル接続し、初期化完了のメッセージが送られてくることを 確認します。
import time,glob
from serial import Serial
ttydev=glob.glob("/dev/tty*usb*")[0]
port=Serial(ttydev, baudrate=115200)
rply=port.read_until(b'*')
print(f"{ttydev=}",rply, rply.decode('ascii'))
ttydev='/dev/tty.usbmodem11101' b'\r\ngainermodoki32u Ready:0.0.1.08*' gainermodoki32u Ready:0.0.1.08*
オープンしたシリアルポートの基本的な設定を確認してみましょう(.get_settings()
メソッド)。
baudrateは既定値の9600から115200に変更されていますが、それ以外は既定値が使われています。
print(f"{port.get_settings()=}")
port.get_settings()={'baudrate': 115200, 'bytesize': 8, 'parity': 'N', 'stopbits': 1, 'xonxoff': False, 'dsrdtr': False, 'rtscts': False, 'timeout': None, 'write_timeout': None, 'inter_byte_timeout': None}
注) ボーレイト(baud rate)について:
ボー (baud) は、変調レートの単位である。ボーは、搬送波に対する1秒間あたりの変調の回数と定義されています。 現在のシリアル通信では、位相と振幅の組み合わせなどを使う事で、1変調あたりに複数のビット情報を送信できます。
シリアル通信の初期には1変調あたり1ビットの方式(BPSK)であったため、1 baud = 1bps(bit per second)でした。
しかし現在では、QPSKから256QAMに至る多値の変調方式がとられるため 1 baud $\ne$ 1 bps となっています。
このような歴史から、古い文献ではbaud
とbps
を混同して使っていることがありますが、そういった歴史的事情を知った上でスルーしましょう。
なお、ボー(baud)は電気通信の初期に功績のあった ジャン=モーリス=エミール・ボドー(Jean-Maurice-Émile Baudot,1845 フランス) にちなんで名付けられたそうです。
注)接続速度について:
ここではArduinoにダウンロードされたプログラムに合わせてbaudrate=115200
を使っていますが、USB-CDC接続ではもっと
速い接続で通信することも可能です。可能な最高通信速度は接続する装置、操作するPC側のインタフェースの種類、ドライバなどによって変わります。
利用環境ごとに確認が必要です。
次に3色LEDの赤を点灯してみます。コマンド"a300"
をデバイスに送信します。
その後、応答"a30*"
を待ちます。
port.write(b"a300")
rply=port.read_until(b'*') # depend on version of
print("raw:", rply,"\t as string:", rply.decode('utf-8'))
raw: b'a30*' as string: a30*
これらの動作は、Arduino固有のものではなく、現在使用中のArduinoに書き込まれたプログラム(gainermodoki32u)によって規定されています。
Serial
オブジェクトの.read_until()
メソッドは、シリアルポートからの送信データに指定された文字(ここでは "*"
)が現れるまで、pythonプログラムの実行をブロックします。
次に、コマンド"a3ff"を使って, LEDを消灯させましょう。
print(f"{port.isOpen()=}")
port.write(b"a3ff")
rply=port.read_until(b'*')
print("raw:", rply,"\t as string:", rply.decode('utf-8'))
port.close()
print(f"{port.isOpen()=}")
port.isOpen()=True raw: b'a3FF*' as string: a3FF* port.isOpen()=False
ちょっと余談:
今見ていただいた例では、LEDを点灯するのには”0”, 消灯するのに "0xff"(つまり255)を使っています。
自然なインタフェースでは、”0”と書けば消灯、大きな数字ほど明るくなる期待されます。
これは、この装置で利用した素子の仕様のためにこうなっています。
この装置でのアナログ出力の3番は3色LEDの赤のピンに繋がれています。 この3色LEDではアノード側が三つのLEDで共有されており、グラウンド側が独立となっています。 このため、ボードのアナログ出力はこれら のグラウンド側のピンに接続せざるを得ません。 此の結果、ボードからの出力がフルスケールの時LEDにかかる電圧差は0となり 消灯、ボードからの出力が0VのときLEDはフル点灯となります。
制御プログラム側でこの様な事情を吸収して、"自然”な動作のインタフェースを作ることが可能です(後述)。 制御プログラムの作成ではこのような機器の制限からくる動作を理解しておく必要があります。
基本的な動作が確認できました。
これらを組み合わせて全てのLEDを順次点灯していくプログラムを作ってみましょう。
コマンドを送信し、応答("*"
で終わる)を待つ関数wait_input()
を定義しておきます。
import time
from serial import Serial
def wait_input(port,msg=None):
if port.in_waiting >0:print(port.read_all())
if msg:
port.write(msg)
resp=port.read_until(b'*')
return resp
3色LEDのそれぞれの色および二つの単色LEDを順次点灯するプログラムを作ってみましょう。
コマンドのリストmsgs
にLED の点灯、消灯のコマンドを用意しておき、
これらのコマンドを順次送信します。コマンドとコマンドの間には、wait
秒の間を置くようにします。
def main(n=3, wait=0.1):
ttydev=glob.glob("/dev/tty*usb*")[0]
msgs=(b"a300", b"a3ff",
b"a500", b"a5ff", b"a600", b"a6ff",
b"a900", b"a9ff", b"aa00", b"aaff")
with Serial(ttydev, 115200) as port:
print(wait_input(port).decode('utf-8'))
for i in range(n):
for msg in msgs:
resp=wait_input(port, msg)
time.sleep(wait)
print(i, resp.decode('utf-8'))
print(f"{port.isOpen()=}")# with文を使ったので、portは自動的にクローズされます。
これを実行してみましょう。
print(f"started at {time.strftime('%X')}")
main(3, 0.2)
print(f"finished at {time.strftime('%X')}")
started at 10:48:50 gainermodoki32u Ready:0.0.1.08* 0 aAFF* 1 aAFF* 2 aAFF* port.isOpen()=False finished at 10:49:00
このプログラムでは、Serial
オブジェクトはwith
文の中だけでつかわれています。
明示的に.close()
メソッドの呼び出しは行なっていませんが、with
文を抜けたところでは、すでにSerial
オブジェクトは閉じられている(closed)ことがわかります。
LEDの操作コマンドで、"00"を設定するとLEDがOnになり"0xFF"を設定すると消灯されると いうのは、直感に反しますので、0-255(0xff)の数値でLEDの 明るさを設定する関数を定義します。
import time
from serial import Serial
def wait_input(port,msg=None):
if port.in_waiting >0:print(port.read_all())
if msg: port.write(msg)
return port.read_until(b'*')
def setRled(port, r):
r=max(0,min(0xff,r))
r = 0xff-r
msg=f"aA{r:02x}"
return wait_input(port, msg.encode('ascii'))
この関数を使って、赤色LEDの明るさを徐々に増減するプログラムを作ってみましょう。
def main():
ttydev=glob.glob("/dev/tty*usb*")[0]
with Serial(ttydev, baudrate=115200) as port:
print(wait_input(port).decode('utf-8')) # Arduinoの起動を待つ。
print("start increasing")
for r in range(0,0x100,4):
setRled(port,r)
print("decreasing")
for r in reversed(range(0,0x100,4)):
setRled(port,r)
print("end")
main()
gainermodoki32u Ready:0.0.1.08* start increasing decreasing end
赤のLED(0xA=10)だけではなく緑のLED(9)も同時に制御してみましょう。 共通の操作は別関数としてまとめておきます。
import time
from serial import Serial
def wait_input(port,msg=None):
if port.in_waiting >0:print(port.read_all())
if msg: port.write(msg)
return port.read_until(b'*')
def setLed(port, chan, r):
r=max(0,min(0xff,r))
r = 0xff-r
msg=f"a{chan:1x}{r:02x}"
return wait_input(port, msg.encode('ascii'))
def setRled(port,r):
return setLed(port,10,r)
def setGled(port,r):
return setLed(port,9,r)
これらの関数を使い、赤色LEDと緑色LEDの明るさを
def main():
ttydev=glob.glob("/dev/tty*usb*")[0]
with Serial(ttydev, baudrate=115200) as port:
print(wait_input(port).decode('utf-8'))
print("start")
setGled(port,0)
setRled(port,0xff)
for r in range(0,0x100,4):
setRled(port,0xff-r)
setGled(port,r)
for r in reversed(range(0,0x100,4)):
setRled(port,0xff-r)
setGled(port,r)
setRled(port,0)
setGled(port,0)
print("end")
for i in range(1):
main()
gainermodoki32u Ready:0.0.1.08* start end
第13回(2022/3/8)はここまで
第14回(2022/3/29)はここから
* Arduinoは 6本 の アナログ入力ピン を持っています。
* ADCは10bits精度(0-1023)を持っています。
* ボードに与えられた基準電圧をフルスケール(1023)とした整数値が返されます。
指定されたアナログピン番号(pin
)の入力電圧を読み出して、端末に印刷するプログラムを作ってみましょう。データを読み出した時間を印刷するために、time.ctime()
関数を使っています。
import time, glob
from serial import Serial
from math import log
def wait_input(port,msg=None):
if port.in_waiting >0:print(port.read_all())
if msg: port.write(msg)
return port.read_until(b'*')
def readADC(port,pin):
msg=f"S{pin:1x}"
resp=wait_input(port,msg.encode('ascii'))
val=int(resp[2:-1])
return val
Arduinoのアナログ入力を読み込むコマンドは"S<pin番号>"です。"<pin番号>" は16進一桁の数字で指定します。読み込んだデータをint
関数を使って、0-1023の整数に変換します。
電圧を読み取り、明るさ(arb. unit) に変換して印刷する関数Cds()
を定義します。
CdSセルはアナログ入力ピン 0 に接続されています。
def Cds(N=10,wait=1):
ttydev=glob.glob("/dev/tty*usb*")[0]
chan=0 # CdS
with Serial(ttydev, baudrate=115200) as port:
print(wait_input(port).decode('utf-8')) # Arduinoの起動を待つ。
for i in range(N):
v=readADC(port,chan)
L=v/(1023-v) # Luminance with an arbitrary scale
print(f"{time.ctime()}: {v=} {L=:.2f} a.u.")
time.sleep(wait)
電圧を読み取り、温度に変換して印刷する関数Therm()
を定義します。
Thermistorはアナログ入力ピン 1 に接続されています。
def Therm(N=10,wait=1):
ttydev=glob.glob("/dev/tty*usb*")[0]
C=6.5 # いい加減な較正係数
A=2370
chan=1 # Thermister
with Serial(ttydev, baudrate=115200) as port:
print(wait_input(port).decode('utf-8')) # Arduinoの起動を待つ。
for i in range(N):
v=readADC(port,chan)
TC=A/(C+log((1023-v)/v))-273.15
print(f"{time.ctime()}: {v=} {TC=:.2f} °C")
time.sleep(wait)
係数 $A, C$ は何点か異なる温度で測定を行った結果から計算しています。
これらの関数を実行してみましょう。
Cds(10)
Therm(10)
gainermodoki32u Ready:0.0.1.08* Tue Mar 29 16:29:58 2022: v=865 L=5.47 a.u. Tue Mar 29 16:29:59 2022: v=865 L=5.47 a.u. Tue Mar 29 16:30:01 2022: v=866 L=5.52 a.u. Tue Mar 29 16:30:02 2022: v=865 L=5.47 a.u. Tue Mar 29 16:30:03 2022: v=717 L=2.34 a.u. Tue Mar 29 16:30:04 2022: v=569 L=1.25 a.u. Tue Mar 29 16:30:05 2022: v=557 L=1.20 a.u. Tue Mar 29 16:30:06 2022: v=865 L=5.47 a.u. Tue Mar 29 16:30:07 2022: v=866 L=5.52 a.u. Tue Mar 29 16:30:08 2022: v=865 L=5.47 a.u. gainermodoki32u Ready:0.0.1.08* Tue Mar 29 16:30:10 2022: v=163 TC=17.18 °C Tue Mar 29 16:30:11 2022: v=162 TC=16.92 °C Tue Mar 29 16:30:13 2022: v=161 TC=16.66 °C Tue Mar 29 16:30:14 2022: v=162 TC=16.92 °C Tue Mar 29 16:30:15 2022: v=163 TC=17.18 °C Tue Mar 29 16:30:16 2022: v=163 TC=17.18 °C Tue Mar 29 16:30:17 2022: v=163 TC=17.18 °C Tue Mar 29 16:30:18 2022: v=163 TC=17.18 °C Tue Mar 29 16:30:19 2022: v=164 TC=17.44 °C Tue Mar 29 16:30:20 2022: v=167 TC=18.21 °C
この装置でデジタル出力をHighにするには、"H
import time, glob
from serial import Serial
def wait_input(port,msg=None):
if port.in_waiting >0:print(port.read_all())
if msg: port.write(msg)
return port.read_until(b'*')
def dout(port,pin,val):
if val:
cmd="H"
else:
cmd="L"
msg=f"{cmd:1s}{pin:1x}"
print(msg)
resp=wait_input(port,msg.encode('ascii'))
return resp
def main():
ttydev=glob.glob("/dev/tty*usb*")[0]
chan=2 # Buzzer
with Serial(ttydev, baudrate=115200) as port:
print(wait_input(port).decode('utf-8')) # Arduinoの起動を待つ。
dout(port,13,True)
dout(port,chan,True)
time.sleep(3)
dout(port, chan, False)
dout(port, 0xd, False)
main()
gainermodoki32u Ready:0.0.1.08* Hd H2 L2 Ld
このArduinoにダウンロードされているプログラムでは、"H2"
コマンドは特別扱いされていて、ブザーを使って音階を鳴らします。
デジタル入力の例として、switchを監視して、状態が変化(On->Off/Off->On)された時に端末にメッセージを
印刷するプログラムを作成してみましょう。 "R"
コマンドを使って、指定するデジタル入力ピン番号の状態を読み込みます。int()
を使ってbyte
データを整数に変換します。
import time, glob,datetime
from serial import Serial
from math import log
def wait_input(port,msg=None):
if port.in_waiting >0:print(port.read_all())
if msg: port.write(msg)
return port.read_until(b'*')
def din(port,pin):
msg=f"R{pin:1x}"
resp=wait_input(port,msg.encode('ascii'))
val=int(resp[2:-1])
return val
デジタル入力の7番ピンに接続したプッシュスイッチの状態をモニタしてみましょう。
前回の状態をst_prev
に記録して、新しく読み込んだ状態をst
に保存します。
これらがお互いに異なる値の時、メッセージを出力します。
datetime.datetime.now().ctime()
はdatetime
モジュールのdatetime
クラスの.now()
メソッドを
使って現在時間を入手し、.ctime()
関数で標準の時刻を表す文字列に変換しています。
.ctime()
を.isoformat()
に変えると、秒以下の時間まで印刷できるようになります。
def main(N=10,wait=0.1):
ttydev=glob.glob("/dev/tty*usb*")[0]
chan=7 # push switch # chan=12 # tilt switch
with Serial(ttydev, baudrate=115200) as port:
wait_input(port).decode('utf-8') # Arduinoの起動を待つ。
st=din(port,chan)
print(f"start: {datetime.datetime.now().ctime():s}")
for i in range(N):
st_prev,st=st,din(port,chan)
if st_prev != st:
print(
f"{datetime.datetime.now().isoformat()[:-3]:s} st: {st_prev} -> {st}"
)
time.sleep(wait)
print(f"end: {datetime.datetime.now().ctime():s}")
main(100,0.05)
start: Tue Mar 29 16:35:42 2022 2022-03-29T16:35:51.204 st: 0 -> 1 2022-03-29T16:35:51.605 st: 1 -> 0 end: Tue Mar 29 16:35:52 2022
Arduinoから読み込んだデータの時間変化をstrip chart風に表示してみます。
まずは、matplotlibを使って、グラフを作成してみます。
それに先立ち、何度も使うArduinoに関する関数を一つのファイル(arduino_utils.py
)にまとめておきます。 このファイルをモジュールとしてimport
することで、これらの関数を別のプログラムで利用できるようになります。
(このプログラムを実行したArduinoにはアナログ入力の3番に別の照度センサをつないでいます。)
%%file arduino_utils.py
import time, glob
from serial import Serial
from math import log
def wait_input(port, msg=None):
if port.in_waiting >0:print(port.read_all())
if msg: port.write(msg)
return port.read_until(b'*')
def readADC(port, pin):
msg=f"S{pin:1x}"
resp=wait_input(port,msg.encode('ascii'))
val=int(resp[2:-1])
return val
def getLum(dev):
chan=3 # Lum
v=readADC(dev, chan)
return v
def getCds(dev):
chan=0 # CdS
v=readADC(dev, chan)
L=30*v/(1023-v) # Luminance with an arbitrary scale
return L
def getTherm(dev):
C=6.5 # いい加減な較正係数
A=2370
chan=1 # Thermister
v=readADC(dev,chan)
TC=A/(C+log((1023-v)/v))-273.15
return TC
Overwriting arduino_utils.py
matplotlibで時間変化をグラフ化する際には、横軸の変数としてdatetime.datetime
オブジェクトを使うのが便利です。
まずは、データをリストに蓄積し、matplotlibでグラフ化してみます。
%matplotlib widget
from arduino_utils import *
import datetime
import matplotlib.pyplot as pyplot
def plot_test(N=10):
ttydev=glob.glob("/dev/tty*usb*")[0]
if (not ttydev):
return
print(f"connecting to {ttydev}")
with Serial(ttydev, baudrate=115200) as dev:
wait_input(dev).decode('utf-8') # Arduinoの起動を待つ。
lt=[datetime.datetime.now()]
lcds=[getCds(dev)]
llum=[getLum(dev)]
lines=pyplot.plot(lt,lcds,lt,llum)
for i in range(100):
lt.append(datetime.datetime.now())
lcds.append(getCds(dev))
llum.append(getLum(dev))
lines[0].set_data((lt,lcds))
lines[1].set_data((lt,llum))
pyplot.xlim(min(lt),max(lt))
pyplot.ylim(0, 1.05*max(max(lcds),max(llum)))
time.sleep(0.2)
pyplot.draw()
(グラフの全体を見るためにはブラウザでズームアウトをお試しください。)
%matplotlib widget
plot_test()
pyplot.show()
connecting to /dev/tty.usbmodem11101
時間変化のグラフを動的なアニメーションとして表示してみます。
グラフのアニメーションを表示するために、matplotlibではanimation
サブモジュールが提供されています。今回はanimation
の中のFuncAnimation
クラスを使うことにします。
FuncAnimation
のオブジェクトの生成には、
* グラフを描画する`Figure`オブジェクトと
* アニメーションの各フレームを更新するための関数を引数に与えます。
* グラフを初期化するための関数 を`init_func`に与えることもできます。
# for widget, install ipympl with `pip install ipympl`
from arduino_utils import *
import datetime,logging
import matplotlib.pyplot as pyplot
from matplotlib.animation import FuncAnimation
import IPython
from IPython.display import display,HTML
# equivalent to rcParams['animation.html'] = 'html5'
#from matplotlib import rc
#rc('animation', html='html5')
def anim(dev, mxl=200):
fig, ax = pyplot.subplots(1,1)
ax.relim()
ax.autoscale_view()
lt=[]
lcds=[]
llum=[]
def anim_plot(t1):
nonlocal fig, ax, mxl, dev
nonlocal lt, lcds, llum
lines=ax.lines
lt.append(datetime.datetime.now())
lcds.append(getCds(dev))
llum.append(getLum(dev))
lt=lt[-mxl:]
lcds=lcds[-mxl:]
llum=llum[-mxl:]
lines[0].set_data((lt,lcds))
lines[1].set_data((lt,llum))
ax.set_xlim(min(lt), max(lt))
ax.set_ylim(0, 1.05*max(max(lcds),max(llum)))
return ax
def setup():
nonlocal fig, ax, dev
nonlocal lt, lcds, llum
logging.info("start setup")
lt=[datetime.datetime.now()]
lcds=[getCds(dev)]
llum=[getLum(dev)]
ax.plot(lt, lcds, lt, llum)
return ax
ani=FuncAnimation(fig,
anim_plot,init_func=setup,
frames=max(300,mxl),
interval=200, blit=False,
)
return ani
%matplotlib widget
if __name__ == "__main__":
ttydev=glob.glob("/dev/tty*usb*")[0]
if (not ttydev):
import sys
sys.exit()
print(f"connecting to {ttydev}")
with Serial(ttydev, baudrate=115200) as dev:
wait_input(dev).decode('utf-8') # Arduinoの起動を待つ。
ani=anim(dev,120)
display(HTML(ani.to_html5_video())) # jupyterlabで表示させるためのおまじない
pyplot.close()
connecting to /dev/tty.usbmodem11101
前節ではアニメーションをファイルに保存して、そのファイルを端末上で表示しました。
以下のプログラムを実行することで、実時間に変化するグラフを表示することができます。
端末で、python3 function_animation.py
を実行します。コメントを外すことによって、アニメーションを保存することもできます。保存するファイルの形式として、HTML5, gif, mp4がサポートされています。
%%file function_animation.py
#!python3
import datetime, logging
import matplotlib.pyplot as pyplot
import matplotlib.animation as mpl_anim
from arduino_utils import *
def anim(dev,mxl=200):
fig, ax = pyplot.subplots(1,1)
ax.relim()
ax.autoscale_view()
lt=[]
lcds=[]
llum=[]
def anim_plot(values):
nonlocal fig, ax, mxl, dev
nonlocal lt, lcds, llum
lines=ax.lines
lt.append(values[0])
lcds.append(values[1])
llum.append(values[2])
lt=lt[-mxl:]
lcds=lcds[-mxl:]
llum=llum[-mxl:]
lines[0].set_data((lt,lcds))
lines[1].set_data((lt,llum))
ax.set_xlim(min(lt), max(lt) )
ax.set_ylim(0, 1.05*max(max(lcds),max(llum)) )
return [ax,]
def gen_function():
nonlocal fig, ax, mxl, dev
nonlocal lt, lcds, llum
while 1:
yield(
datetime.datetime.now(),
getCds(dev),
getLum(dev)
)
def setup():
nonlocal fig, ax, mxl,dev
nonlocal lt, lcds, llum
logging.info("start setup")
lt=[datetime.datetime.now()]
lcds=[getCds(dev)]
llum=[getLum(dev)]
ax.plot(lt, lcds, lt, llum)
pyplot.draw()
return [ax,]
ani=mpl_anim.FuncAnimation(fig, anim_plot, init_func=setup,
frames=gen_function, interval=200,
save_count=2*mxl, cache_frame_data=False,
)
pyplot.show()
# with open("_images/Figure_1.html","w") as f :
# f.write(ani.to_html5_video())
# ani.save("_images/Figure_1.gif", writer="pillow")
# ani.save("_images/Figure_1.mp4", writer="ffmpeg")
return ani
def main():
ttydev=glob.glob("/dev/tty*usb*")[0]
if (not ttydev):
import sys
sys.exit()
print(f"connecting to {ttydev}")
with Serial(ttydev, baudrate=115200) as dev:
wait_input(dev).decode('utf-8') # Arduinoの起動を待つ。
ani=anim(dev,120)
pyplot.close()
if __name__ == "__main__":
main()
Overwriting function_animation.py
実行中に保存されたグラフのイメージ
端末を開き、
python3 function_animation.py
を実行して、実時間でアニメーショングラフを表示します。 (グラフの窓を閉じるなどでプログラムの実行は停止されます。)
import IPython
from IPython.display import display,HTML
display(HTML(open("_images/Figure_1.html").read()))
今回で2021年度のPython入門講座は終了です。
テキストの資料は、
http://j-parc.jp/ctrl/documents/articles/
から入手可能です。(随時更新/追加 することがあります。)
録画資料もこちらから閲覧できるように整備する予定です(なお、MS teamsのゲストアカウントは取り消しとなる予定です。Streamも利用できなくなります。)
EPICS CAをPythonから利用するための講座を準備中です。準備が整いましたら、ご連絡いたします。