Дачный логгер отработал две недели, и понадобился файл на компьютер. Корпус собран, панелька карты утоплена за распаянным блоком питания, кардридер остался дома в ящике, а при себе — ноутбук с одним USB-портом и шнуром. Данные лежат в тридцати сантиметрах от разъёма. Читать их скетчем я уже умел: в уроке про available и read мы листали лог глазами в мониторе, считали байты и строки. Но читать глазами две недели данных — не работа, а наказание: файл должен приехать на компьютер целым, и не в виде скриншотов монитора.
Дорога — через Serial, который у платы и так есть. Скетч ловит команду dump из монитора, открывает log.csv на чтение и выдавливает содержимое в порт строка за строкой; на компьютере его ловит приёмник — от ручного копипаста до Python-скрипта на pyserial, который собирает строки обратно в файл. Полный разбор такого приёмника — в уроке про Arduino и Python; здесь заберём протокол и скетч платы, а приёмник положим рядом в минимальном виде.
В уроке: протокол дампа, который не пачкает данные служебными строками, честный подсчёт минут на выгрузку при 9600 и 115200 бод, LED-прогресс и приёмник в десять строк. В симуляторе выгружается сгенерированный скетчем «лог» — сам цикл available/read от настоящей карты живёт в code-блоке ниже, карта движку не по зубам.
Протокол: сырой CSV и решётка для служебных
Приёмник должен отличать данные от болтовни платы, иначе в файл приедут приветствия и размеры вперемешку с температурами. Договариваемся так: строки лога едут как есть — они уже CSV, — а любые служебные сообщения платы начинаются с решётки: «# DUMP BEGIN size=155520», «# DUMP END», «# SD error». Решётка выбрана не из эстетики: в моём логе из чисел и запятых её нет, поэтому фильтр приёмника в одну строку не зацепит данные. Заведёте текстовый лог с хэшами — берите другой префикс; договорённость важнее символа.
Команда. Скетч слушает порт: в буфере появился символ d — начинается выгрузка. Во время дампа логирование приостанавливается флагом: читать и писать один файл одновременно можно, но курсоры и две открытые сессии превращают простой скетч в головоломку, а пара пропущенных строк за две минуты выгрузки никого ещё не сожгла. По окончании поток данных продолжается сам — флаг снимается, счётчики идут дальше.
Скорость канала — главный лимит. 9600 бод — это примерно 960 байт в секунду: сутки лога при интервале десять секунд — 8640 строк по восемнадцать байт, около 155 КБ, — едут две с половиной минуты. 115200 бод сжимают то же до четырнадцати секунд — двенадцать раз быстрее, и это честный аргумент поднять скорость в скетче дампа. Запас у скетча при этом тает: буфер приёма 64 байта и его переполнение мы считали в уроке про скорость записи, но там плата слушала, а здесь только говорит — односторонний дамп переполнить нечем.
#include <SPI.h>#include <SD.h> const int chipSelect = 10;const int LED = 12; // вспышка на каждый чанк 256 байтbool dumping = false; void setup() { Serial.begin(9600); pinMode(LED, OUTPUT); if (!SD.begin(chipSelect)) { Serial.println("# SD error"); while (true) { } }} void loop() { // команда dump: символ d в мониторе порта if (Serial.available() > 0) { char c = Serial.read(); if (c == 'd' && !dumping) { dumpLog(); } } // здесь обычный цикл логгера; флаг dumping пропускает // такты записи, пока файл едет в порт} void dumpLog() { dumping = true; File f = SD.open("log.csv", FILE_READ); if (!f) { Serial.println("# open failed"); dumping = false; return; } Serial.print("# DUMP BEGIN size="); Serial.println(f.size()); unsigned long bytes = 0; while (f.available()) { // условие тает с каждым байтом char b = f.read(); Serial.write(b); bytes = bytes + 1; if (bytes % 256 == 0) { // LED-прогресс digitalWrite(LED, HIGH); delay(5); digitalWrite(LED, LOW); } } f.close(); Serial.println(); Serial.print("# DUMP END bytes="); Serial.println(bytes); dumping = false;}Разбор чтения. available() тает с каждым read(), поэтому while останавливается ровно на конце файла без ручных границ; read() возвращает int, но -1 здесь не встретится — available() гарантирует байт. Печать — Serial.write(b), а не print: write отдаёт байт как есть, без числовых преобразований, и CSV на приёмнике получается побайтово тем же, что на карте. Размер файла из size() печатается в строке BEGIN — приёмник заранее знает, сколько ждать, и может показать процент.
LED-прогресс: вспышка на каждые 256 байт — на 9600 бод это раз в четверть секунды, глаз видит ровный темп. Замирание диода — сигнал затыка: чаще всего порт на компьютере занял второй читатель. Если лог растёт сутками, файл упрётся в десятки мегабайт, и дамп растянется до получаса: имена по дням и ротацию логов мы разбирали в отдельном уроке — выгружайте по одному дню, протокол от этого не меняется. Ленивый путь — отдать файл из браузера по Wi-Fi: веб-логи на ESP32 живут в соседнем уроке, но там нужен другой чип.
Целостность принятого файла проверяется двумя числами: size из строки BEGIN и bytes из строки END. Приёмник сравнивает их в конце: совпали — файл приехал целиком; не совпали — где-то посреди канала была дыра, и выгрузку лучше повторить, чем лечить файл потом. Контрольной суммы я не держу: для текстового CSV хватает счётчиков, а честный CRC потянул бы протокол на уровень выше. Проценты удобно печатать прямо в мониторе: size известен с первой строки, bytes считаются на лету — прогресс «37 из 100» выводите раз в килобайт, чаще — уже шум. Для чтения до маркера у pyserial есть и read_until — вариант, когда держать цикл вручную не хочется.
Если логгер не должен молчать даже две минуты, дамп поднимают после кнопки старт-стоп: сессия закрылась — файл ровный, выгрузка идёт спокойно, следующее нажатие откроет новую сессию. У меня так и устроено в гараже: кнопка на старт-стоп, буква d на дамп, и оба оператора — я сам — ничего не путаем уже второй сезон. Совмещать запись и выгрузку в одном потоке на Uno можно, но каждая из них по отдельности и так стоит пауз — зачем умножать.
| Способ выгрузки | Что нужно | Сутки лога, 155 КБ |
|---|---|---|
| Кардридер | доступ к панельке карты, разбор корпуса | секунды, но корпус разобрать |
| Serial-дамп (этот урок) | USB-кабель, который и так воткнут | 2,5 мин на 9600, 14 с на 115200 |
| Веб-логи на ESP32 | ESP32 с Wi-Fi и скетч-сервер | секунды из браузера, другой чип |
Приёмник на компьютере: pyserial в десять строк
Скорость дампа выбирается под задачу. Дефолтные 9600 бод удобны тем, что работают везде: любой адаптер, любой шнур, любая длина провода. 115200 бод стоит включать, когда выгрузкой занимаетесь регулярно: четырнадцать секунд против двух с половиной минут за две недели набегают в часы. Свою цифру прикиньте по строке BEGIN: size из неё делите на девятьсот шестьдесят — столько секунд файл едет на 9600 бод. Выше подниматься смысла не вижу: на Uno между байтами нет пауз только в одностороннем канале, а провода длиннее полуметра на 115200 уже заставляют нервничать — сначала короткий шнур, потом скорость.
import serial PORT = "COM5" # Linux: /dev/ttyUSB0ser = serial.Serial(PORT, 9600, timeout=2)out = open("log.csv", "w") while True: line = ser.readline().decode("ascii", errors="ignore").strip() if line == "": continue if line.startswith("#"): print(line) # служебные строки - на экран if line.startswith("# DUMP END"): break else: out.write(line + "\n") # данные - в файл out.close()ser.close()print("готово: log.csv")Каркас приёмника совпадает со скетчем платы строка в строку: readline() ждёт перевод строки, всё с решёткой уходит на экран и служит статусом, всё без решётки дописывается в файл, строка «# DUMP END» закрывает цикл. Пауза timeout=2 в pyserial — не про скорость, а про то, что readline() не зависнет навечно, если плата молчит. С фильтрацией приёмник переживает и мусор в канале: строку, разрезанную посреди байта, вы выбросите по редкому «#», а частичную строку данных увидите глазами — у меня за год таких набралось две, обе на момент просадки питания.
Пара строк про транспорт. readline() режет по переводу строки и тянет за собой возврат каретки — strip() в каркасе снимает хвостовые символы, без него в конце строк сидят невидимые \r. Имя порта на Windows — COM5 из диспетчера устройств, на Linux — /dev/ttyUSB0; если устройств ttyUSB несколько, порядок после переподключения не гарантирован — стабильное имя по чипу живёт в /dev/serial/by-id. Кодировка: лог у меня цифровой, ascii хватает; кириллицу в лог не пишу — половина байта на символ и разрезы посреди букв, о которых подробно говорили в уроке про кириллицу в Serial.
- Дамп через Serial требует только USB, который и так воткнут: плата читает файл и печатает его в порт — кардридер и разбор корпуса не нужны
- Протокол: данные — сырыми строками CSV, вся служебная болтовня — с префиксом # , тогда фильтр приёмника не зацепит данные
- 9600 бод — примерно 960 байт/с: сутки лога в 155 КБ едут 2,5 минуты; 115200 бод сжимают до 14 секунд
- LED-вспышка на каждые 256 байт — видимый темп выгрузки; замирание — затык, чаще всего второй читатель на порту
- Во время дампа логирование приостанавливается флагом: писать и читать один файл одновременно можно, но зачем головоломка
- 1. Зачем служебным строкам платы префикс # ?
- 2. Сколько времени едет файл в 155 КБ на 9600 бод?
- 3. Что делает флаг dumping в скетче дампа?
- 4. Почему цикл while (f.available()) останавливается ровно на конце файла?
- 5. Порт занял монитор IDE, Python-приёмник запустили следом. Что окажется в файле?
Частые вопросы по теме урока
- Зачем служебным строкам платы префикс # ?
- Данные едут сырым CSV, где решётки нет, поэтому фильтр startswith(#) в одну строку пропускает лог нетронутым, а статусы выводит на экран. чтобы приёмник отделял болтовню от данных лога и не записал её в файл
- Сколько времени едет файл в 155 КБ на 9600 бод?
- 9600 бод — примерно 960 байт в секунду; 155520 байт делятся на 960 — выходит около 160 секунд. На 115200 бод тот же файл занимает четырнадцать секунд. около 2,5 минуты
- Что делает флаг dumping в скетче дампа?
- Две сессии на один файл — курсоры и открытые хэндлы вперемешку. Пара пропущенных строк за минуту выгрузки дешевле головоломки из совмещённых режимов. пропускает такты записи логгера, пока файл читается в порт
- Почему цикл while (f.available()) останавливается ровно на конце файла?
- available() показывает байты от курсора до конца; каждый read() сдвигает курсор, поэтому условие останавливается без ручных границ — механика из урока про чтение файла. available() тает с каждым read() — условие ложнеет на конце файла
- Порт занял монитор IDE, Python-приёмник запустили следом. Что окажется в файле?
- Читатель на порту один: два приложения делят байты непредсказуемо, и в файл приедет дырявый лог. Перед запуском приёмника монитор закрывают. половина строк — байты разъехались по двум читателям