Три сезона я записывал показания бака с балкона руками: монитор, глаза, блокнот. Когда весной понадобился график за зиму, в блокноте нашлось сорок строк из примерно сорока тысяч — остальное никто не записал, данные ушли в монитор и растворились. Вечер с Python закрыл вопрос навсегда: порт открылся как файл, строки поехали в CSV, график построился из тех же строк. Урок про эту связку: Arduino печатает данные в порт, скрипт на компьютере их читает, и между ними нет ничего, кроме кабеля и пары правил.
План такой. Поставим pyserial и прочитаем первые строки; договоримся о формате CSV, который плата печатает, а скрипт режет; разберём имена портов — COM-номера Windows против /dev/ttyUSB0 в Linux — и самую частую граблю старта: порт занят монитором Arduino IDE, читатель может быть только один. Python-код урока выполняется на вашем компьютере — в браузерном симуляторе он не запускается, зато Arduino-часть в симе напечатает ровно те строки, которые скрипт получит с платы.
pyserial: порт как обычный файл
Библиотека зовётся pyserial, ставится командой pip install pyserial — и вот первая грабля: пакет serial в реестре лежит чужой и старый, ставить нужно именно pyserial, хотя импортируется модуль всё равно словом serial. Открытие порта выглядит как открытие файла: port = serial.Serial("COM3", 9600, timeout=1). Скорость обязана совпадать с Serial.begin на плате: при расхождении readline не найдёт перевода строки в чужом потоке байтов, и вы получите ту же кашу, что в мониторе при несовпадении скоростей. Связка чтения — три звена: readline() ждёт байты до перевода строки, decode("utf-8") превращает байты в текст, strip() снимает невидимые хвосты вроде возврата каретки.
Дальше строка режется: stroka.split(",") возвращает список кусков — для строки 512,2.50 это два элемента: «512» и «2.50». Пробел после запятой съел бы второй кусок, поэтому CSV печатается без пробелов. На плате то же самое делают parseInt и parseFloat — мы разбирали числа из монитора в отдельном уроке; Python честнее: float("2.50") не молчит на мусоре, а падает вслух, и для логов это благо.
Про живой график: значения собираются в список, matplotlib рисует линию, plt.pause(0.1) возвращает управление окну — и график оживает. Для лога раз в секунду readline за глаза; если плата печатает быстрее сотни строк в секунду, readline начинает тормозить, и потоки читают пачками через port.read(port.in_waiting) — но это уже другая лига. И следите за очередью: буфер приёма на Uno — 64 байта; если скрипт уснул, а плата печатает, хвост потеряется — та же механика, что в уроке про буфер UART.
Четвёртое звено, которое я долго понимал неправильно, — timeout=1 в открытии порта. Это не период и не пауза, а предел ожидания: readline стоит у порта максимум секунду и отдаёт всё, что успело прийти, а при тишине — пустую строку. Плате с печатью раз в секунду этого хватает с запасом: на 9600 бод строка из двадцати байтов занимает около двадцати миллисекунд, и readline просыпается сразу по переводу строки, а не по таймеру. Ставите timeout больше — скрипт вяло реагирует на выдёргивание кабеля; меньше — на медленном потоке режете строки на середине.
# Chtenie CSV iz porta. Kod vypolnyaetsya NA KOMPYUTERE: v brauzernom# simulyatore on ne zapuskaetsya — tam pokazyvaet Arduino-chast iz sim-bloka.# Ustanovka: pip install pyserial (imenno pyserial, ne serial).import serial port = serial.Serial("COM3", 9600, timeout=1) # Linux: /dev/ttyUSB0 while True: stroka = port.readline().decode("utf-8").strip() if stroka == "": continue # timeout: nichego ne prishlo chasti = stroka.split(",") if len(chasti) == 2: znachenie = int(chasti[0]) volty = float(chasti[1]) print("znachenie =", znachenie, "| U =", volty, "V")Скелет выше — учебный. Живой скрипт держит ошибки: плата висит на USB с проводом до макетки, я задеваю кабель локтем пару раз за вечер, и без обработки readline поднимает SerialException, а скрипт умирает вместе с недописанным логом. Обвязка простая: цикл чтения внутри try, ловим SerialException и KeyboardInterrupt, в finally — port.close(). Вторая грабля из той же серии: скрипт упал, порт не закрылся, следующий запуск падает с отказом — Windows отпускает порт через пару секунд, Linux держит до завершения процесса. Очередь к порту работает в обе стороны: пока в питоне открыт serial.Serial, монитор IDE не откроется тоже — читатель один, и я закрываю скрипт перед каждым взглядом в монитор.
Лог я пишу сразу в CSV: файл открывается в режиме добавления, split даёт значения, writerow кладёт их строкой в файл. Одна грабля стоила мне вечера данных: Python буферизует запись, и при Ctrl+C хвост остаётся в памяти — последние минуты в файл не доезжают. Лечится flush после каждой записи; при строке раз в секунду цена нулевая. Дальше всё собирается само: try вокруг цикла, внутри — разбор, запись, печать хода; в finally — close у порта и файла. Такой скрипт переживает задетый кабель и Ctrl+C, а поверх файла matplotlib рисует кривую из тех же списков — те самые три строки из предыдущего абзаца.
Сам график в развёрнутом виде: рядом с циклом два пустых списка, каждый проход дополняет их числами из split, после остановки — plt.plot(znach, volty) и plt.show(). Для живой картинки вместо show вызывают plt.pause(0.1) в каждом проходе — окно перерисовывается, и провал питания видно раньше, чем он попадёт в файл. Мой порядок на стыковке обратный: график открываю для глаз, а лог пишу всегда — файл переживёт закрытое окно, окно переживёт не каждый вечер.
CSV с платы и имена портов
Формат я выбрал машинный: значение с потенциометра и вольты — через запятую, без пробелов и подписей: 512,2.50. Пары вида znachenie=512 — язык для глаз и для плоттера; CSV — язык для машины: скрипт режет его по запятым и складывает в таблицу, как в уроке про таблицу данных, только на стороне компьютера. Светодиод в скетче мигает на каждой отправленной строке — пульс потока видно из-за комнаты: лампочка дышит, значит, данные едут.
Имена портов — второй подводный камень после скорости. На Windows плата зовётся COM3 или COM4, и номер плавает: переткнули кабель в другое гнездо — стала COM5, скрипт падает; актуальный номер смотрят в Диспетчере устройств, а в скрипте держат параметром командной строки. На Linux это /dev/ttyUSB0 для плат с мостом CH340 и /dev/ttyACM0 для плат с аппаратным USB вроде Uno R3; права на порт выдаёт группа dialout — мы настраивали её в уроке про Linux и udev. На macOS порт оканчивается на usbmodem, и плат в списке две — tty и cu; берите cu. Таблица ниже — шпаргалка, которую я держу в закладках.
| Система | Как зовут порт | Подводный камень |
|---|---|---|
| Windows | COM3, COM4… | номер плавает при смене гнезда USB; смотреть в Диспетчере устройств |
| Linux | /dev/ttyUSB0 или /dev/ttyACM0 | нужна группа dialout; после пере-подключения номер может вырасти |
| macOS | /dev/tty.usbmodem… | плат в списке две — tty и cu; берите cu |
// CSV dlya kompyutera: znachenie,volty. Bez probelov: split(",")// na storone Python reshit vse po zapyatoi.int period = 1000; // stroka raz v sekundu void shliStroku() { int syr = analogRead(A0); float volty = syr * 5.0 / 1023.0; Serial.print(syr); Serial.print(","); Serial.println(volty); digitalWrite(12, HIGH); // puls potoka: stroka ushla delay(60); digitalWrite(12, LOW);} void setup() { Serial.begin(9600); pinMode(A0, INPUT); pinMode(12, OUTPUT); Serial.println("nachalo: znachenie,volty");} void loop() { shliStroku(); delay(period);}В симуляторе Python-часть не эмулируется — это честное ограничение движка: браузерный журнал показывает Arduino-сторону, а скрипт вы запустите на своём компьютере против настоящей платы. Хорошая новость: строки в журнале — байт в байт те, что прочитает readline(). Ручка потенциометра на макетке гоняет первое число CSV: крутите её и смотрите, как значение ползёт в строках вместе с ручкой — analogRead мы разбирали в разделе про основы. Светодиод мигает на каждой строке: в реальной сессии по нему видно, что поток жив, даже если скрипт ещё молчит.
Покрутите период: команда period 300 убыстрит печать до трёх строк в секунду, period 2000 замедлит; команда odin выпускает одну строку по требованию. Представьте на месте журнала ваш скрипт: readline() получил бы ровно эти строки, split(",") дал бы два куска, и значения уже можно рисовать. Дальше — за вами: закройте монитор, запустите скрипт против настоящей платы, и первый живой CSV-лог уедет в файл за десять строк кода.
- pyserial ставится командой pip install pyserial: пакет serial — чужой, а импортируется всё равно serial
- Скорость в скрипте и в Serial.begin обязаны совпасть: иначе readline не найдёт перевод строки и придёт каша
- readline().decode("utf-8").strip() — базовая связка; split(",") режет CSV, и плата печатает его без пробелов
- Порт держит один читатель: открыт монитор — Python получит отказ; закрывайте монитор и перед заливкой скетча
- Имена портов плавают: Windows COM-N, Linux ttyUSB0/ttyACM0 с группой dialout, macOS usbmodem и пара tty/cu
- 1. pyserial установили, скрипт падает с отказом доступа к порту. Первое подозрение?
- 2. В Serial.begin стоит 115200, а в скрипте 9600. Что увидит Python?
- 3. Плата напечатала строку 512,2.50. Что вернёт stroka.split(",")?
- 4. На Linux скрипту запрещено открывать /dev/ttyUSB0. Что делать?
- 5. Зачем в связке с pyserial упоминают matplotlib?
Частые вопросы по теме урока
- pyserial установили, скрипт падает с отказом доступа к порту. Первое подозрение?
- У порта один читатель. Закройте монитор IDE — и скрипт откроется; PuTTY и miniterm стоят в той же очереди. Монитор Arduino IDE держит порт: читатель один
- В Serial.begin стоит 115200, а в скрипте 9600. Что увидит Python?
- При расхождении скоростей приёмник собирает чужие байты, терминатор не находится — readline отдаёт мусор. Скорости сверяются первыми. Кашу из символов: перевод строки не найдётся
- Плата напечатала строку 512,2.50. Что вернёт stroka.split(",")?
- split режет строку по запятой и возвращает список строк; дальше int() и float() превращают куски в числа. Список из двух строк: «512» и «2.50»
- На Linux скрипту запрещено открывать /dev/ttyUSB0. Что делать?
- Права на порт даёт группа dialout; root — костыль, который забудется и выстрелит позже. Настройку udev разбирали в разделе про IDE. Добавить пользователя в группу dialout
- Зачем в связке с pyserial упоминают matplotlib?
- Значения из split собираются в список, matplotlib рисует линию, plt.pause возвращает управление окну — график оживает. Он рисует живой график из значений, собранных скриптом