ガイド · 読了まで20分

ArduinoとPythonのシリアル通信:pyserial、CSV、リアルタイムグラフ

pyserialを使ったArduinoとPythonのシリアル通信を解説します。インストール、ポートの確認、センサーデータのCSV保存、コマンド送信、matplotlibでのリアルタイムグラフまで紹介します。

ArduinoとPythonのシリアル通信とは、パソコンで動くPythonのプログラムが、USBケーブルを通してArduinoとデータをやり取りすることです。Arduinoがセンサーを測定して結果をシリアルポートに書き込み、Pythonがその行をpyserialライブラリで読み取って、CSVファイルに保存したり、グラフにしたり、判断してArduinoにコマンドを送り返したりします。こうすることで、Arduinoの正確なハードウェア制御と、データ処理、グラフ、AI、自動化に強いPythonの力を組み合わせられます。追加のモジュールも必要ありません。スケッチを書き込むときのUSBケーブルだけで十分です。

このガイドでは、pyserialのインストール、Arduinoのポートの自動検出、センサーデータをタイムスタンプ付きでCSVファイルに記録する方法、PythonからLEDやPWMのコマンドを送る方法、matplotlibでリアルタイムグラフを描く方法を順を追って学びます。そのあと、データ形式と改行コードの選び方、もっとも混乱しやすい2つの問題(ポート使用中エラーと、ポートを開いたときにボードがリセットされる問題)、そしてRaspberry Pi PicoのMicroPythonで同じことをする場合の違いを解説します。例はArduino Uno向けに書いてあり、ESP32とPicoで必要な変更は別に紹介します。

Arduino Uno R3ボードとUSBソケット
Arduino Uno:USBソケットはコードの書き込みにも、Pythonとのシリアル通信にも使います。データが流れている間はTXとRXのLEDが点滅します。

ArduinoはどうやってPythonと通信するのか?

Arduino UnoのUSBシリアル変換チップ(純正品ではATmega16U2、互換品ではたいていCH340)は、パソコンでは仮想シリアルポートとして認識されます。WindowsではCOM3、COM4など、macOSでは/dev/cu.usbmodem…や/dev/cu.usbserial-…、Linuxでは/dev/ttyACM0や/dev/ttyUSB0です。ArduinoがSerial.print()で書き込む文字はすべて、バイトとしてこのポートを通ってパソコンに流れます。Python側では、pyserialがポートをファイルのように開いて読み書きします。大事な区別が1つあります。Arduino UnoはPythonのコードを実行しません。ボードはC++で書かれたArduinoのコードを、パソコンはPythonを実行し、両者はシリアル回線を通して会話するだけです。両側が同じ言葉で話すには、次の設定が一致している必要があります。

設定Arduino側Python側注意点
ポートUSB接続で自動的に作られるserial.Serial("COM3", ...)ポート名はOSによって異なる
速度(ボーレート)Serial.begin(115200)serial.Serial(port, 115200)違っていると文字化けする
フレームデータ8ビット、パリティなし、ストップビット1(8N1)pyserialの初期設定も8N1変更が必要になることはほとんどない
改行コードSerial.println()は\r\nを付けるreadline()は\nが来るまで読むPythonから送るときは\nを付け忘れない
タイムアウトSerial.setTimeout()、初期値1000 mstimeout=1(秒)timeout=Noneだとreadline()はいつまでも待つ

pyserialのインストール(Windows、macOS、Linux)

  1. Python 3がインストールされているか確認します。Windowsでは、Python公式サイトのインストーラーの最初の画面でAdd python.exe to PATH(PATHに追加)にチェックを入れます。macOSとLinuxには、たいていPython 3が最初から入っています。
  2. ターミナル(WindowsではコマンドプロンプトかPowerShell)を開き、python -m pip install pyserialと入力します。Windowsがpythonコマンドを認識しない場合はpy -m pip install pyserialを試してください。macOSとLinuxでは、コマンドがpython3 -m pip install pyserialになる場合があります。
  3. インストールを確認します:python -c "import serial; print(serial.__version__)"を実行して、3.5のようなバージョン番号が表示されればOKです。
  4. リアルタイムグラフの章ではmatplotlibも必要です:python -m pip install matplotlib。
  5. Linuxでは、ポートを開けるようにユーザーをdialoutグループに追加します:sudo usermod -a -G dialout $USERを実行してから、ログアウトしてログインし直します。Raspberry Pi OSや最近のUbuntuでpipがexternally-managed-environmentエラーを出す場合は、sudo apt install python3-serialを使うか、python3 -m venvで仮想環境を作り、その中でpipを実行します。
PyPIには、シリアルポートとは無関係のserialという別のパッケージがあります。pip install serialと入力してしまうと、コードはimport serialの行は通りますが、その後AttributeError: module 'serial' has no attribute 'Serial'(Serialという属性がない)で止まります。正しいパッケージはpyserialです。pip uninstall serialで間違ったほうを削除し、pyserialを入れ直してください。自分のファイルにserial.pyという名前を付けた場合も同じエラーが出ます。Pythonがライブラリの代わりにそのファイルを読み込んでしまうためです。

Arduinoはどのポート?Pythonでポートを見つける

いちばん簡単なのはArduino IDEの「ツール」>「ポート」メニューを見る方法ですが、Python自身でポートを一覧表示することもできます。ターミナルでpython -m serial.tools.list_ports -vと入力すると、すべてのポートの名前、説明、ハードウェアID(VID:PID)が表示されます。どの行がArduinoかわからない場合は、ボードを抜いてもう一度コマンドを実行し、消えたポートを確認します。macOSでは同じボードが/dev/tty…と/dev/cu…の両方で表示されます。Pythonではcuで始まるほうを使ってください。

CH340 USB-TTL変換モジュール
CH340チップ:互換品のArduinoボードの多くでは、このチップがUSBとシリアルポートを橋渡しします。ベンダーID(VID)は1A86です。
USBチップ搭載されているボードVID:PIDWindowsでの表示名Linuxのポート名
ATmega16U2純正品のUno R3、Mega 25602341:0043(Uno)Arduino Uno (COMx)/dev/ttyACM0
CH340互換品のUno、Nano、Mega1A86:7523USB-SERIAL CH340 (COMx)/dev/ttyUSB0
CP2102ESP32 DevKit、NodeMCU10C4:EA60Silicon Labs CP210x USB to UART Bridge (COMx)/dev/ttyUSB0
FT232R古いNano、FTDIケーブル0403:6001USB Serial Port (COMx)/dev/ttyUSB0
RP2040(内蔵USB)MicroPythonを入れたRaspberry Pi Pico2E8A:0005USB シリアル デバイス (COMx)(英語版ではUSB Serial Device)/dev/ttyACM0

次の関数はすべてのポートを一覧表示し、VIDからボードを認識した場合は、そのボードのポート名を返します。コードをport_bul.py(トルコ語で「ポートを探す」)として保存すれば、毎回ポート名を手で書き換える代わりに、他のスクリプトでfrom port_bul import port_bulとPORT = port_bul()と書くだけで済みます。

Python
1import serial.tools.list_ports
2
3# Bilinen USB yongalarının üretici kimlikleri (VID) / Vendor IDs of common USB chips
4BILINEN = {
5    0x2341: "Arduino (orijinal / genuine)",
6    0x2A03: "Arduino (orijinal / genuine)",
7    0x1A86: "CH340 (klon / clone)",
8    0x10C4: "CP210x (ESP32, NodeMCU)",
9    0x0403: "FTDI",
10    0x2E8A: "Raspberry Pi Pico",
11}
12
13
14def port_bul():
15    portlar = sorted(serial.tools.list_ports.comports())
16    for p in portlar:
17        print(p.device, "|", p.description, "|", p.hwid)   # tüm portları listele / list every port
18    for p in portlar:
19        if p.vid in BILINEN:
20            print("Bulundu / found:", p.device, "->", BILINEN[p.vid])
21            return p.device
22    return None
23
24
25if __name__ == "__main__":
26    if port_bul() is None:
27        print("Kart yok: kabloyu ve surucuyu kontrol et / no board: check cable and driver")

Arduinoからデータを送る:最初のスケッチ

例では、ピンA0にポテンショメーターをつなぎます。中央のピンをA0に、外側のピンの一方を5Vに、もう一方をGNDにつなぎます。ポテンショメーターの代わりに、LDRを10 kΩの抵抗と組み合わせて分圧回路としてつなぐこともできます(出力電圧は分圧計算ツールで確認できます)。スケッチは200 msごとに値を読み取り、結果を時間,値の形式で1行にまとめて送ります。

C++ (Arduino IDE)
1// A0 değerini "millis,deger" CSV satırı olarak gönderir / Sends the A0 reading as a "millis,value" CSV line
2const int SENSOR_PIN = A0;            // potansiyometrenin orta ucu / potentiometer wiper
3const unsigned long ARALIK = 200;     // gönderim aralığı, ms / send interval, ms
4unsigned long sonGonderim = 0;
5
6void setup() {
7  Serial.begin(115200);               // Python'daki hızla aynı olmalı / must match the baud rate in Python
8}
9
10void loop() {
11  if (millis() - sonGonderim >= ARALIK) {   // delay() olmadan zamanlama / timing without delay()
12    sonGonderim = millis();
13    int deger = analogRead(SENSOR_PIN);     // 0-1023
14    Serial.print(sonGonderim);              // zaman damgası / timestamp
15    Serial.print(',');                      // ayırıcı / separator
16    Serial.println(deger);                  // sonuna \r\n ekler / appends \r\n
17  }
18}

delay()の代わりにmillis()を使うと、あとでPythonからのコマンドを読み取る処理を同じループに追加しやすくなります。詳しくはmillis()とdelay()の違いを見てください。書き込んだあと、シリアルモニタを115200ボーで開くと、2573,512のような行が流れていきます。これは確認のためだけです。Pythonのスクリプトを実行する前には、必ずシリアルモニタを閉じてください。閉じないとポートが使用中になります。シリアルモニタが初めてなら、Arduinoのシリアルモニタのガイドが役に立ちます。

PythonでArduinoのデータを読み取り、CSVに保存する

Python
1import csv
2import time
3from datetime import datetime
4
5import serial
6
7PORT = "COM3"     # macOS: /dev/cu.usbmodem14101, Linux: /dev/ttyACM0
8BAUD = 115200     # Arduino'daki Serial.begin ile aynı / same as Serial.begin on the Arduino
9
10with serial.Serial(PORT, BAUD, timeout=1) as ser, \
11        open("olcumler.csv", "w", newline="", encoding="utf-8") as dosya:
12    time.sleep(2)               # Uno port açılınca resetlenir / the Uno resets when the port opens
13    ser.reset_input_buffer()    # yarım kalmış eski veriyi at / drop stale, partial data
14    yazici = csv.writer(dosya)
15    yazici.writerow(["tarih", "zaman_ms", "deger", "volt"])   # başlık satırı / header row
16    print("Kayit basladi, durdurmak icin Ctrl+C / logging, press Ctrl+C to stop")
17    try:
18        while True:
19            satir = ser.readline().decode("utf-8", errors="ignore").strip()
20            parcalar = satir.split(",")
21            if len(parcalar) != 2:
22                continue                    # boş ya da beklenmeyen satır / empty or unexpected line
23            try:
24                zaman_ms, deger = int(parcalar[0]), int(parcalar[1])
25            except ValueError:
26                continue                    # bozuk satır / corrupted line
27            volt = deger * 5.0 / 1023       # Uno ADC: 0-1023 -> 0-5 V
28            yazici.writerow([datetime.now().isoformat(timespec="seconds"),
29                             zaman_ms, deger, round(volt, 3)])
30            dosya.flush()                   # satırı hemen diske yaz / write the row to disk now
31            print(zaman_ms, deger, f"{volt:.2f} V")
32    except KeyboardInterrupt:
33        print("Durduruldu / stopped")

このスクリプトはtimeout=1でポートを開きます。readline()は行末を最大1秒待ち、何も届かなければ空のbytesオブジェクトを返すので、ループが止まってしまうことはありません。受信したデータはbytes型です。decode()でテキストに変換し、strip()で末尾の\r\nを取り除き、split(",")で行を列に分けます。ポートを開いたあとのtime.sleep(2)とreset_input_buffer()は、Unoが再起動している間に届く途中までの行を捨てるためのものです。理由はこのあと説明します。ファイルをnewline=""で開くとWindowsで空行が入るのを防げ、flush()を使うと、プログラムが突然終了しても、それまでの測定値がディスクに保存されます。記録を止めるには、ターミナルでCtrl+Cを押します。

CSVファイルがExcelで1列にまとまって開く場合、原因は地域設定です。ヨーロッパの多くの地域設定では、Excelはリストの区切り文字としてセミコロン、小数点としてカンマを想定します(日本語環境の標準設定はカンマ区切りなので、通常はそのまま開けます)。その場合はcsv.writer(dosya, delimiter=";")を使い、電圧をstr(round(volt, 3)).replace(".", ",")で書き込むか、Excelの「データ」>「テキストまたはCSVから」でファイルを読み込み、区切り文字を自分で選んでください。Excelにデータを取り込むほかの方法は、ArduinoのデータをExcelに送る方法を見てください。

PythonからArduinoにコマンドを送る

通信は双方向です。下のArduinoスケッチは、1行ずつ届くコマンドを待ちます。LED ONとLED OFFで内蔵LEDを点灯・消灯し、PWM 128でピン9のLEDの明るさを設定し、READでA0の値を送り返します。どのコマンドにも短い返事が返るので、Pythonはコマンドが本当に処理されたことを確認できます。ピン9のLEDに合う抵抗はLED抵抗計算ツールで求められます。PWMのしくみはPWMとは?で解説しています。

C++ (Arduino IDE)
1// Python'dan gelen satır komutlarını işler / Handles line-based commands from Python
2const int LED_PIN = LED_BUILTIN;      // Uno'da pin 13 / pin 13 on the Uno
3const int PWM_PIN = 9;                // 220 ohm dirençli LED / LED with a 220 ohm resistor
4
5void setup() {
6  pinMode(LED_PIN, OUTPUT);
7  pinMode(PWM_PIN, OUTPUT);
8  Serial.begin(115200);
9  Serial.setTimeout(50);              // readStringUntil en fazla 50 ms bekler / waits at most 50 ms
10  Serial.println("HAZIR");            // Python bu satırı bekler / Python waits for this line
11}
12
13void loop() {
14  if (Serial.available() > 0) {
15    String komut = Serial.readStringUntil('\n');   // satır sonuna kadar oku / read up to the newline
16    komut.trim();                                  // \r ve boşlukları sil / strip \r and spaces
17
18    if (komut == "LED ON") {
19      digitalWrite(LED_PIN, HIGH);
20      Serial.println("OK LED ON");
21    } else if (komut == "LED OFF") {
22      digitalWrite(LED_PIN, LOW);
23      Serial.println("OK LED OFF");
24    } else if (komut.startsWith("PWM ")) {
25      int deger = komut.substring(4).toInt();     // "PWM 128" -> 128
26      deger = constrain(deger, 0, 255);           // 0-255 aralığında tut / keep it within 0-255
27      analogWrite(PWM_PIN, deger);
28      Serial.print("OK PWM ");
29      Serial.println(deger);
30    } else if (komut == "READ") {
31      Serial.println(analogRead(A0));              // A0 ölçümünü gönder / send the A0 reading
32    } else if (komut.length() > 0) {
33      Serial.print("ERR ");                        // bilinmeyen komut / unknown command
34      Serial.println(komut);
35    }
36  }
37}
Python
1import time
2
3import serial
4
5
6def komut_gonder(ser, komut):
7    ser.write((komut + "\n").encode("ascii"))       # satır sonu şart / the newline is required
8    return ser.readline().decode("ascii", errors="ignore").strip()   # cevabı bekle / wait for the reply
9
10
11with serial.Serial("COM3", 115200, timeout=2) as ser:
12    bitis = time.time() + 3                         # en fazla 3 sn bekle / wait at most 3 s
13    while time.time() < bitis:
14        if ser.readline().strip() == b"HAZIR":      # Arduino açıldı / the Arduino is up
15            break
16    print(komut_gonder(ser, "LED ON"))              # OK LED ON
17    time.sleep(1)
18    print(komut_gonder(ser, "PWM 128"))             # OK PWM 128
19    print("A0 =", komut_gonder(ser, "READ"))        # A0 = 512
20    print(komut_gonder(ser, "LED OFF"))             # OK LED OFF

Python側でもっとも多い間違いは、コマンドの末尾の\nを付け忘れることです。readStringUntil('\n')が行末を見つけられないと、ArduinoはSerial.setTimeout()で設定した時間だけ待ってから、ようやくコマンドを処理します。このスケッチではその時間は50 msですが、初期値の1000 msのままだと、どのコマンドも1秒遅れることになります。write()はbytesしか受け付けないので、テキストはencode()で変換するか、前にbを付けて(b"LED ON\n")bytesにする必要があります。スクリプトは最初にHAZIR(トルコ語で「準備完了」)の行を最大3秒待ちます。Unoはポートを開くと再起動するため、すぐに送ったコマンドはブートローダーに届いて失われてしまうからです。コマンドを送るたびに返事を読む、つまりリクエストとレスポンスの形にすると、両側の同期が保たれ、バッファがあふれるのも防げます。Unoの受信バッファはわずか64バイトです。

データ形式と改行コード:どれを選ぶ?

シリアル回線が届けるのはバイトの流れだけです。1つの測定値がどこで終わり、次がどこから始まるかを示すのは自分の役目です。もっとも確実なのは、測定値を1つずつ別の行に書き、どの行も\nで終える方法です。行の中でどの形式を使うかは、プロジェクトの規模によって決まります。

形式行の例長所短所
単一の値512もっとも簡単な形式。Arduinoのシリアルプロッタでそのままグラフにできる値が複数あると、どれが何かわからない
CSV2573,512,23.5短く、Excelやpandasでそのまま読める列の順番を両側でそろえておく必要がある
キー:値temp:23.5,hum:41読みやすく、シリアルプロッタにラベルが表示される長くなり、解析に少し手間がかかる
JSON{"t":23.5,"h":41}Pythonではjson.loads()の1行でデコードできるもっとも長い形式。Unoの限られたRAMを圧迫する
バイナリ2バイト:0x02 0x00もっとも速く、短い方法人間には読めない。structモジュールと開始バイトが必要

改行コードも混乱の原因になりやすい部分です。Serial.println()は各行の末尾に2文字を付けます。復帰(\r、CR)と改行(\n、LF)です。Pythonのreadline()は\nしか待たないので、行の末尾に\rが残ります。strip()を使えば両方とも取り除けます。Arduino IDEのシリアルモニタの改行コードのリストには改行なし、LFのみ、CRのみ、CRおよびLFがあります。Pythonを使う前にシリアルモニタでArduinoのスケッチをテストするときは、Pythonから送るのと同じ文字が送られるようにLFのみを選んでください。小数はSerial.print(temperature, 1)のように桁数を指定して出力します。Unoのsprintfは浮動小数点の書式(%f)に対応していないため、sprintfで小数を出力しようとすると、数値の代わりに疑問符(?)が表示されます。

ボーレート1秒あたりのバイト数(8N1)1秒あたりの12バイトの行数使いどころ
960096080遅いセンサー、長いケーブル、古いサンプルスケッチ
576005,760480中程度の速度の測定
11520011,520960リアルタイムグラフ、ESP32、ほとんどの新しいプロジェクト

シリアル回線では、各バイトがスタートビットとストップビットを含めて10ビットで送られるので、115200ボーはおよそ1秒あたり11,520バイトになります。25730,1023に改行コード(\r\n)を加えた12バイトの行なら、1秒に最大960行まで送れます。両側の速度が違うと文字化けした文字が流れ、PythonはUnicodeDecodeErrorを出します。1秒に5〜20回の測定で、人間の目にもほとんどのセンサーにも十分です。必要以上に頻繁に送ると、グラフの動きが遅くなり、ファイルも大きくなります。

matplotlibでリアルタイムグラフを描く

Python
1from collections import deque
2
3import matplotlib.pyplot as plt
4import serial
5from matplotlib.animation import FuncAnimation
6
7ser = serial.Serial("COM3", 115200, timeout=0.1)
8N = 200                                          # ekrandaki ölçüm sayısı / readings on screen
9veri = deque([0] * N, maxlen=N)                  # eskiler kendiliğinden düşer / old values drop out
10
11fig, ax = plt.subplots()
12cizgi, = ax.plot(range(N), list(veri))
13ax.set_ylim(0, 1023)                             # Uno ADC aralığı / Uno ADC range
14ax.set_xlabel("Ornek / sample")
15ax.set_ylabel("A0")
16
17
18def guncelle(_):
19    while ser.in_waiting:                        # biriken bütün satırları oku / read every queued line
20        parca = ser.readline().decode("utf-8", errors="ignore").strip().split(",")
21        if len(parca) == 2 and parca[1].isdigit():
22            veri.append(int(parca[1]))
23    cizgi.set_ydata(list(veri))
24    return (cizgi,)
25
26
27ani = FuncAnimation(fig, guncelle, interval=50, blit=True, cache_frame_data=False)   # ani silinmemeli / keep ani
28plt.show()
29ser.close()

グラフは、直近200個の測定値を保持するdequeの上に作られています。新しい値が右に追加されると、いちばん古い値は自動的に左から押し出されます。FuncAnimationは50 msごとにguncelle(トルコ語で「更新」)関数を呼び出します。関数の中のwhile ser.in_waitingループが重要です。現在Arduinoは1秒に5行送っていますが、送信頻度を上げたときに1フレームで1行しか読まないと、グラフが実際の時間から遅れていきます。このループは待っている行をすべて読み、グラフを最新の状態に保ちます。ani変数は削除しないでください。アニメーションのオブジェクトを参照する変数がないと、Pythonのガベージコレクションで削除され、ウィンドウが固まってしまいます。ESP32ではset_ylim(0, 4095)、Picoではset_ylim(0, 65535)を使います。さっと確認したいだけなら、Arduino IDEのシリアルプロッタで十分です。Pythonの利点は、グラフを保存したり、複数のデータ源を組み合わせたり、同時にデータを処理したりできることです。

ポート使用中エラーと自動リセットの問題

シリアルポートは、一度に1つのプログラムしか開けません。Pythonのスクリプトを実行している間に、Arduino IDEのシリアルモニタやシリアルプロッタが開いていたり、別のPythonスクリプトがポートを使っていたり、3Dプリンター用ソフトのようにポートをスキャンするソフトがバックグラウンドで動いていたりすると、Windowsではcould not open port 'COM3': PermissionError(13, 'アクセスが拒否されました。', None, 5)(英語版Windowsでは'Access is denied.')、macOSとLinuxではResource busyやDevice or resource busyというエラーが出ます。逆も同じで、Pythonのスクリプトがポートを開いている間は、Arduino IDEからコードを書き込めません。そのため、スクリプトではwith serial.Serial(...) as ser:の構文を使いましょう。ブロックが終わったときやエラーが起きたときに、ポートが自動的に閉じます。書き込む前にCtrl+Cでスクリプトを止めることを習慣にしてください。

2つ目の問題はもっとわかりにくいものです。Arduino Uno、Nano、Megaでは、USBチップのDTRラインが100 nFのコンデンサを介してプロセッサのRESETピンにつながっています。この回路は、書き込み時にIDEがボードを自動で再起動できるようにするためのものですが、pyserialがポートを開くたびにDTRも変化し、ボードがリセットされます。ボードはまずブートローダーで1〜2秒待ち、それからスケッチを最初から実行します。その間に送ったコマンドは失われ、Arduinoがsetup()で出力する最初の行を取りこぼすこともあります。対策は、ポートを開いたあとtime.sleep(2)で待つことです。さらに確実なのは、ArduinoがHAZIRのような行を送ってくるのを待つ方法で、上のコマンドの例はまさにそうしています。ポートはプログラムの最初に1回だけ開き、最後まで開いたままにしてください。測定のたびにポートを開け閉めするループでは、ボードが何度もリセットされてしまいます。

リセットを完全に防ぎたい場合は、2つの方法があります。ソフトウェアでは、ser = serial.Serial()でポートを閉じた状態で作り、ser.portとser.baudrateを設定し、ser.dtr = Falseと書いてからser.open()を呼び出します。これはWindowsではたいてい効果がありますが、LinuxとmacOSでは、ポートを開くときにOSがDTRを切り替えてしまうことがあります。ハードウェアでは、UnoのRESETピンとGNDピンの間に10 µFのコンデンサを入れ、プラス側の足をRESETにつなぎます。Leonardo、Micro、Raspberry Pi PicoのようにUSBを内蔵したボードは、ポートを開いても再起動しません。これらのボードでは、Pythonが接続する前にコードが出力した行が失われることがあるので、重要なメッセージはコマンドが届いたときに送るほうが安全です。

10 µFのコンデンサを付けたままだと、Arduino IDEからコードを書き込めません。書き込みも同じ自動リセット回路を利用しているためです。書き込む前にコンデンサを外してください。Pythonがdevice reports readiness to read but returned no data(読み取り可能と報告されたがデータが返されなかった)というエラーを出す場合は、ボードの接続が切れているか、2つのプログラムが同時にポートを使っています。動作中にボードが勝手にリセットされる場合、原因はたいてい電源不足です。サーボやモーターには、USBの5Vではなく別の電源から電力を供給してください。

ESP32でのPythonシリアル通信

ここで紹介したPythonのコードは、すべてESP32でも動きます。変更が必要なのはArduino側の数か所だけです。アナログ入力にはADC1のピンのどれか(例:GPIO 34)を使います。ESP32のADCは12ビットで、0〜3.3 Vを0〜4095に変換するので、Pythonではdeger * 3.3 / 4095で電圧を計算します(degerはトルコ語で「値」)。起動時、ESP32はrst:0x1 (POWERON_RESET),boot:0x13のような独自の起動メッセージを115200ボーで出力しますが、上のスクリプトは数値に変換できない行を読み飛ばすので、問題にはなりません。ボードの自動リセット回路やOSによっては、ESP32もポートを開いたときに再起動することがあるので、ここでも待ち時間とHAZIRの行が役に立ちます。USBチップはたいていCP2102かCH340です。ケーブルを使いたくない場合は、ESP32なら同じデータをWi-Fiで送れます。ESP32のWebSocketとESP32とGoogleスプレッドシートのガイドを見てください。初めてのESP32なら、まずESP32とは?を読みましょう。

Raspberry Pi PicoとMicroPython:何が違う?

Raspberry Pi Pico開発ボード
Raspberry Pi Pico:RP2040にUSBペリフェラルが内蔵されているので、別のUSBシリアル変換チップは不要です。MicroPythonを入れると、パソコンからはそのままシリアルポートとして認識されます。

Pythonとハードウェアを組み合わせるもう1つの方法は、ボードの上で直接Pythonを動かすことです。Raspberry Pi PicoはMicroPythonでプログラムできます。センサーの読み取り、LEDの制御、タイミングを、すべてPicoの中でPythonの文法で行えます。ただし、MicroPythonはデスクトップ版Pythonの軽量版で、matplotlib、pandas、OpenCVはPicoでは動きません。そのため、データを記録したり、グラフを描いたり、AIで処理したりしたい場合は、Picoもパソコン上のPythonにつなぎます。この記事のpyserialのコードはPicoでも動きます。以下で説明するように、ポート名、値の範囲、コマンドの返事の読み方を合わせるだけです。

項目Arduino+パソコンのPythonPico+MicroPython(単体)Pico+パソコンのPython
Pythonが動く場所は?パソコンだけ。ボードはC++を実行Picoの中両方:PicoではMicroPython、パソコンではPython
パソコンは必要?必要。データはパソコンで処理する不要。USB電源や電池で動く必要
使えるライブラリpyserial、matplotlib、pandas、numpy、OpenCVなどmachineやtimeなど、限られたモジュールパソコンのあらゆるライブラリ
ボーレート重要。両側で一致させる必要があるシリアルポートを使わない関係ない。データはUSBの速度で流れる
ポートを開いたときのリセットUno、Nano、Megaではありなしなし
選ぶ場面すでにArduinoを持っていて、既存のライブラリが必要なときシンプルな単体の装置両側を1つの言語(Python)にそろえたいとき

次のMicroPythonのコードは、上のArduinoの例のPico版です。GP26のポテンショメーターを200 msごとに読み取り、結果を同じ時間,値の形式で送り、同じLEDのコマンドを受け付けます。ここでは、ArduinoのSerialに相当するのがprint()とsys.stdinです。select.poll()のおかげで、コマンドを待っている間も測定のループが止まりません。

MicroPython
1# Pico'ya main.py adıyla kaydet / save it on the Pico as main.py
2import sys
3import select
4import time
5from machine import ADC, Pin
6
7led = Pin("LED", Pin.OUT)          # Pico ve Pico W dahili LED / onboard LED on Pico and Pico W
8pot = ADC(26)                      # GP26 = ADC0, potansiyometre orta ucu / potentiometer wiper
9okuyucu = select.poll()
10okuyucu.register(sys.stdin, select.POLLIN)   # USB'den veri gelince haber ver / notify when USB data arrives
11
12son = time.ticks_ms()
13while True:
14    if okuyucu.poll(0):                       # bekleyen komut var mı? / is a command waiting?
15        komut = sys.stdin.readline().strip()
16        if komut == "LED ON":
17            led.on()
18            print("OK LED ON")
19        elif komut == "LED OFF":
20            led.off()
21            print("OK LED OFF")
22    if time.ticks_diff(time.ticks_ms(), son) >= 200:   # 200 ms'de bir ölçüm / one reading every 200 ms
23        son = time.ticks_ms()
24        print("{},{}".format(son, pot.read_u16()))     # zaman,değer (0-65535) / time,value (0-65535)

Thonnyでコードをmain.pyとしてPicoに保存したら、Thonnyを閉じて、PicoのUSBケーブルを抜き差しします。ThonnyはPicoのシリアルポートを自分で使うため、接続中はPythonのスクリプトがポートを開けません。また、Thonnyは接続時に実行中のプログラムを止めてしまいます。再起動すると、main.pyが自動的に実行されます。PicoのUSBポートではボーレートは関係なく、Pythonでどんな値を指定しても、データはUSBの速度で流れます。read_u16()は0〜65535の値を返すので、電圧はdeger * 3.3 / 65535で計算します。Picoは測定の行を絶え間なく送るので、上のコマンドのスクリプトのreadline()は、返事ではなく測定値を返すことがあります。Picoでは、OKで始まる行が来るまで読み続けてください。この例にはHAZIRの行も、PWMやREADのコマンドもありません。必要ならPicoのコードに追加してください。パソコンからバイナリデータを送る場合は注意が必要です。MicroPythonでは0x03というバイトがCtrl+Cを意味し、プログラムが止まってしまいます。これはmicropython.kbd_intr(-1)で無効にできます。Picoが初めてなら、Raspberry Pi Picoとは?とMicroPythonの基本から始めるのがおすすめです。

ArduinoとPythonで作るプロジェクトのアイデア

  • 温湿度ロガー:DHT11の測定値を1分に1回CSVに書き込み、部屋の1週間の気候をExcelやpandasでグラフにします。
  • パソコンから照明を制御:Pythonから送るONとOFFのコマンドでリレーモジュールを動かし、タイマーや音声コマンドと組み合わせます。
  • 距離のリアルタイムグラフ:HC-SR04で測った距離をmatplotlibでグラフにします。センサーをサーボで回せば、同じ方法でレーダー画面も描けます。
  • パソコンの状態表示:Pythonがpsutilライブラリで読み取ったCPUとメモリの使用率をArduinoに送り、ArduinoがそれをOLEDディスプレイに表示します。
  • 顔を追いかけるカメラ:OpenCVが画像の中の顔の位置を見つけ、Pythonがそれを角度に変換して送り、Arduinoがサーボモーターでカメラを回します。
  • 自分だけのショートカットキーボード:Arduinoにつないだボタンが押されたら1行送るようにし、Pythonがそれを読み取って、パソコンでプログラムを開いたりショートカットを実行したりします。

よくある間違いと解決方法

エラーや症状原因解決方法
エラー:ModuleNotFoundError: No module named 'serial'(serialモジュールがない)スクリプトを実行しているPythonインタープリターにpyserialがインストールされていない。VS CodeやThonnyが別のPythonを使っていることがある同じインタープリターでpython -m pip install pyserialを実行し、エディタで正しいインタープリターを選ぶ
エラー:AttributeError: module 'serial' has no attribute 'Serial'(Serialという属性がない)間違ったserialパッケージが入っているか、ファイル名がserial.pyになっているpip uninstall serialのあとpip install pyserialを実行し、ファイル名を変える
エラー:could not open port 'COM3': PermissionError(13, 'アクセスが拒否されました。', None, 5)ポートが別のプログラムで開かれている(シリアルモニタ、シリアルプロッタ、2つ目のスクリプトなど)別のプログラムを閉じる。スクリプトではwith構文を使う
エラー:could not open port 'COM5': FileNotFoundError(2, '指定されたファイルが見つかりません。', None, 2)ポート名が間違っているか、ボードが接続されていないpython -m serial.tools.list_ports -vで正しい名前を確認する
エラー:[Errno 13] Permission denied: '/dev/ttyACM0'(アクセス権がない)Linuxのユーザーにシリアルポートへのアクセス権がないsudo usermod -a -G dialout $USERを実行し、ログアウトしてログインし直す
文字化け、またはUnicodeDecodeErrorボーレートが違う、または起動中のノイズ両側で同じ速度を使う。decode(errors="ignore")とreset_input_buffer()を追加する
readline()がいつもb''を返すArduinoが改行コードを送っていない(printlnではなくprintを使っている)、ポートが違う、またはボードがまだ起動中printlnを使い、ポートと速度を確認し、タイムアウトを長くする
エラー:ValueError: invalid literal for int() with base 10(整数に変換できない)途中で切れた行、空の行、想定外の行を数値に変換した変換をtry/exceptで囲み、列の数を確認する
最初のコマンドが無視されるポートを開いたときにUnoがリセットされ、コマンドがブートローダーに届いたHAZIRの行を待つか、ポートを開いたあと2秒待つ
グラフが実際の時間より遅れる1フレームに1行しか読まないので、データがバッファにたまるin_waitingのループですべての行を読み、送信頻度を下げる
スクリプトの実行中にコードを書き込めないPythonがポートを使っているCtrl+Cでスクリプトを止めてから書き込む
うまくいかないときは、まずPythonを外して、同じボーレートのシリアルモニタでArduinoのスケッチをテストします。そこで行が正しく表示されるなら、問題はPython側(ポート名、速度、改行コード、解析)にあります。そこでも文字化けしているなら、先にArduinoのスケッチを直してください。コードがまったく書き込めない場合は、Arduinoに書き込めないときのガイドの手順に従ってください。シリアル通信のハードウェア側、つまりTX、RX、UARTについて知りたい場合は、UARTとは?を見てください。

ArduinoとPythonをシリアルポートで通信させるのは、シンプルなセンサーのプロジェクトを、データを記録し、グラフを描き、パソコンから操作できるシステムに変えるいちばんの近道です。まず1つの値を読んで表示し、次にCSVに保存し、最後にコマンドとグラフを追加しましょう。どのステップでも、ボーレートと改行コードが両側で一致しているか確認してください。BlueGraysアプリには、LDRで光を、DHT11で温度と湿度を、HC-SR04で距離を測るプロジェクトや、2台のArduinoの間でシリアル通信をするプロジェクトが、回路図とコード付きでステップごとに用意されています。Pythonで読み取るデータ源をすばやく作れます。

よくある質問

ArduinoはPythonでプログラムできますか?

Arduino Uno、Nano、Megaの中ではPythonは動きません。これらのボードは、Arduino IDEでC++ベースのArduino言語を使ってプログラムします。Pythonはパソコンで動き、pyserialを使ってシリアルポート経由でボードと通信します。StandardFirmataのスケッチ例を書き込んで、pyFirmata2のようなライブラリでPythonから直接ピンを制御することもできます。ボードの上でPythonを動かしたい場合は、MicroPythonに対応したRaspberry Pi PicoかESP32を選びましょう。

pyserialはどうやってインストールしますか?

ターミナルでpython -m pip install pyserialを実行し、コードにimport serialと書きます。パッケージ名はpyserialですが、インポートするモジュール名はserialです。pip install serialを実行するとまったく別のパッケージが入り、AttributeErrorの原因になります。Linuxでは、さらにユーザーをdialoutグループに追加する必要があります。

ArduinoがどのCOMポートにつながっているか、どうやって調べますか?

Arduino IDEの「ツール」>「ポート」を見るか、ターミナルでpython -m serial.tools.list_ports -vを実行します。Windowsでは、デバイスマネージャーの「ポート(COMとLPT)」でもポートを確認できます。わからない場合は、ボードを抜いてもう一度ポートを一覧表示すると、消えたものがArduinoのポートです。

PythonからArduinoにデータを送るにはどうすればいいですか?

serial.Serialでポートを開き、ser.write(b"LED ON\n")のようにバイトを送ります。テキストはencode()で変換し、改行コードを付けるのを忘れないでください。Arduino側では、Serial.available()で受信データがあるか確認し、Serial.readStringUntil('\n')で行を読み取ります。Unoはポートを開くとリセットされるので、最初のコマンドを送る前に2秒ほど待ちます。

ArduinoのシリアルモニタとPythonを同時に使えますか?

いいえ。シリアルポートは一度に1つのプログラムしか開けず、2つ目のプログラムでは「アクセスが拒否されました」(Access is denied)や「Resource busy」のエラーが出ます。受信したデータをPythonで処理しながら、同時に画面に表示することはできます。2つの別々のチャネルが必要なら、Arduinoに2つ目のUSB-TTL変換モジュールをつなぎ、SoftwareSerialで2つ目のシリアルポートを作ります。

Pythonでポートを開くと、なぜArduinoがリセットされるのですか?

Uno、Nano、Megaでは、USBチップのDTRラインがコンデンサを介してRESETピンにつながっているため、ポートを開くたびにボードが再起動します。この回路は、IDEがコードを書き込むために必要です。ポートを開いたあと2秒待つか、Arduinoから準備完了を知らせる行が届くのを待ってください。リセットを完全に防ぐにはRESETとGNDの間に10 µFのコンデンサを入れる方法がありますが、書き込む前に外す必要があります。

全プロジェクトを見る
LDRで光の強さを測定
#11初級

LDRで光の強さを測定

部屋の明るさを数値で見てみませんか?光に反応する小さな部品、LDRで周囲の光を測定し、Arduinoが0.5秒ごとにその値をコンピューターの画面に表示します。手で覆うと数値が変わる様子を確認できます。自動で点灯する常夜灯などのプロジェクトへの第一歩です。

4.6(81)
139
HC-SR04で距離を測定(シリアルモニタ表示)
#27初級

HC-SR04で距離を測定(シリアルモニタ表示)

HC-SR04センサーは、コウモリが暗闇で道を見つけるのと同じ仕組みで動作します。人には聞こえない音を出し、その反響を捉えて前方の物体までの距離を測ります。Arduinoは0.5秒ごとに距離をセンチメートル単位でコンピューターの画面に表示し、3〜100センチメートルの測定値を示します。駐車センサーや障害物を避けるロボットなどのプロジェクトの基礎になります。

4.6(46)
71
DHT11で温度と湿度を測定
#55中級

DHT11で温度と湿度を測定

部屋の温度は何度で、空気は湿っているでしょうか、それとも乾燥しているでしょうか?DHT11センサーが温度と湿度を測定し、Arduinoが2行のLCDディスプレイに値を表示して、数秒ごとに更新します。センサーに異常があればエラーメッセージが表示されます。自分だけの小さな気象観測装置を作る第一歩です。

4.7(47)
73
ESP32とLDRモジュールで光の強さを読み取る
#113初級

ESP32とLDRモジュールで光の強さを読み取る

LDRモジュールの生のADC値と、暗い状態・明るい状態の基準値から計算した相対的な明るさの割合を、シリアルモニタでリアルタイムに確認します。この割合はルクスの測定値ではありません。モジュールの校正の仕組みを学べます。しきい値の決定に使うため、プロジェクト10の前に取り組むのが適しています。

4.6(36)
55
2台のArduinoで一方向通信
#57中級
プレミアム

2台のArduinoで一方向通信

2台のArduinoを1本のワイヤーで通信させます。一方のボタンを押すともう一方のLEDが点灯し、もう一度押すと消灯します。送信側のボードがボタンの押下を短いメッセージで伝え、受信側のボードはそのメッセージを受け取るとLEDの点灯と消灯を切り替えます。機器同士が情報を伝える仕組みを、最もシンプルな形で確かめられます。

4.4(37)
50
2台のArduinoで双方向通信
#58中級
プレミアム

2台のArduinoで双方向通信

2台のArduinoをケーブルでつないで通信します。片方のボタンを押すともう片方のライトが点灯し、もう一度押すと消灯します。両方のボードで同じプログラムを動かすので、どちらからでもメッセージを送受信できます。ボード同士でメッセージをやり取りする仕組みを、シンプルな構成で学べます。

4.5(20)
26
BluetoothでスマートフォンからLEDを操作
#34初級
プレミアム

BluetoothでスマートフォンからLEDを操作

スマートフォンで部屋の照明をつけたり消したりするための、小さな練習です。Bluetoothモジュールがスマートフォンから送られた文字をArduinoに伝えます。ある文字で赤いLEDが、別の文字で緑のLEDが点灯し、同じ文字を小文字で送ると消灯します。無線操作の仕組みを、ごくシンプルな形で学べます。

4.8(68)
89
ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
#144上級
プレミアム

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)

ESP32で日時付きの温度・湿度記録システムを作ります!DHT22で1分ごとに温度と湿度を読み取り、DS1307 RTCから取得した日時とともにmicroSDカードへCSV形式で記録します。OLEDには最新の温度を大きな文字で表示し、湿度のパーセント値、日時、総記録数も表示します。SDカードの/temp_log.csvファイルはExcel、Google Sheets、Pythonのpandasで開いて分析できます。温室の監視、家や部屋の温度管理、実験室のデータ収集、食品の保管環境や養蜂箱の監視に適しています。

4.7(7)
12