Картридер щёлкнул, ноутбук увидел карту, на ней log.csv на сорок килобайт и log.bin рядом. Двойной клик по первому — и вместо таблицы каша: все показания слиплись в один столбец, температуры справа прилипли текстом, график строить не из чего. Второй файл система открывать отказывается вовсе: предлагает выбрать программу, а блокнот показывает значки, стрелки и обрывки букв. Оба файла пишет один и тот же логгер за один вечер — различие не в железе, а в том, чем каждый из них раскрывать на компьютере.
Здесь разбираю последнюю милю пути чисел: от файла на карте до графика на экране. У CSV — три настройки импорта, после которых столбцы не слипаются, а числа не превращаются в текст. У бинарного дампа — hex-просмотр: смещения, пары байтов, little-endian и ASCII-колонка, по которым дамп читается как открытая книга. Короткая арифметика того, когда байтовый формат экономит место в два и больше раз, — тоже здесь; полная математика форматов уже сделана в уроке про текст против бинарника, пересказывать её не буду.
Оба сюжета — про случай, когда карта приехала с объекта в кармане. Если вынимать её нельзя, тот же лог снимается без картридера: выгрузка через Serial отдаёт файл прямо в терминал, а веб-сервер файлов на ESP32 — прямо в браузер. Колонки и шапку CSV я проектировал в отдельном уроке — считаю, что файл на карте уже лежит, и вопрос только в том, как его раскрыть.
CSV: три настройки импорта
Excel, получив CSV двойным кликом, поступает по своей воле: разделителем считает запятую, десятичным знаком — то, что прописано в региональных настройках Windows. На русской локали это запятая, поэтому 22.3 из скетча прилетает текстом: столбец температур не суммируется, среднее не считается, график молчит. Лечение живёт в мастере импорта: вкладка Данные, Из текста, разделитель — запятая, десятичный — точка. Три клика, повторяются при каждом свежем файле. LibreOffice предлагает тот же диалог, Google Sheets чаще угадывает сам, а блокнот остаётся последней инстанцией: если и в нём строка развалена по запятым правильно, виноват не файл, а настройка импорта.
Вторая засада сидит в самих байтах: всё, что скетч отправляет через print, попадает в файл как есть, байт в байт, без кодировки и маркера языка. Русская шапка ДАТА,ТЕМПЕРАТУРА в UTF-8 занимает по два байта на букву, а Excel без подсказки читает её как Windows-1251 — на экране кракозябры. Лечится двумя путями: держать шапку латиницей — date,time,temp_c переживёт любую локаль — или разово пересохранить файл блокнотом в ANSI. К значениям с запятыми внутри применимо старое правило из журнала в Serial: формат, который режет по запятой, обязан уметь кавычки — или не пускать запятые в поля.
Дамп в hex-просмотре: смещение, байты, ASCII
Блокнот показывает бинарник кашей, потому что часть байтов совпадает с кодами букв и печатается, а часть — нет. Hex-просмотр снимает вопрос целиком: слева смещение в шестнадцатеричном счёте, в центре шестнадцать байтов строки парами, справа — та же строка в ASCII, где буквенные байты видны буквами, а остальные подменены точками. На Windows ставьте HxD, в терминале Linux и macOS работает xxd, в сети есть веб-варианты, куда файл просто перетаскивается мышью. Ничего покупать не нужно, выбор — дело вкуса.
Чтение дампа начинается с подглядывания: запишите в лог значение, которое знаете заранее, и найдите его в файле. Счётчик первой записи 12 и температура 22.3 в байтовом формате выглядят так: 0C 00 и DF 00 — по два байта на поле, младший байт первым, потому что AVR складывает многобайтовые числа в память именно в этом порядке. Нашли пару на нулевом смещении — формат жив, можно строить ридер. Не нашли — сдвиг ищите руками: шапка и переводы строк в текстовом файле смещают отсчёт, а в байтовом всё строго: N-я запись лежит по смещению 4 на N, и seek доставляет её без чтения всего файла.
Про компактность — коротко и с другими числами, чем в уроке-математике: запись из счётчика и температуры занимает 4 байта, та же строка в CSV с переводом строки — 9. Короткие поля дают экономию в два с половиной раза; длинный штамп датой и временем раздувает текстовую строку сильнее, и разница доходит до пятикратной — каждый символ в CSV стоит байт. Оборотная сторона честная: байтовый файл без ридера бесполезен. Если его будут открывать руками другие люди, оставляйте CSV — никакая экономия байтов не окупит чужого часа на расшифровку.
#include <SPI.h>#include <SD.h> const int CS_PIN = 10;const unsigned long INTERVAL = 3000; // строка раз в три секунды File csvFile;File binFile;unsigned long lastRow = 0;int n = 0; void setup() { Serial.begin(9600); if (!SD.begin(CS_PIN)) { Serial.println("карта не поднялась"); while (true) delay(500); } SD.remove("log.csv"); // свежий эксперимент с чистого листа SD.remove("log.bin"); csvFile = SD.open("log.csv", FILE_WRITE); binFile = SD.open("log.bin", FILE_WRITE); csvFile.println("n,temp_c");} void loop() { unsigned long ms = millis(); if (ms - lastRow < INTERVAL) return; lastRow = ms; n = n + 1; int t10 = (analogRead(A1) - 250) * 30 / 75; // условная температура х10 // CSV: символы, по байту на знак плюс перевод строки csvFile.print(n); csvFile.print(","); csvFile.println(t10 / 10.0, 1); // BIN: те же поля байтами, младший байт первым binFile.write((byte)(n & 255)); binFile.write((byte)((n >> 8) & 255)); binFile.write((byte)(t10 & 255)); binFile.write((byte)((t10 >> 8) & 255)); csvFile.flush(); binFile.flush(); Serial.print("строка "); Serial.print(n); Serial.print(": csv "); Serial.print(csvFile.size()); Serial.print(" байт, bin "); Serial.print(binFile.size()); Serial.println(" байт");}import struct with open("log.bin", "rb") as f: raw = f.read() for i in range(len(raw) // 4): n, t10 = struct.unpack_from("<Hh", raw, i * 4) print(n, t10 / 10)Гигиена работы с картой на компьютере стоит отдельного абзаца. Вставили — скопировали — извлекли средствами системы, и только потом открыли копию: правка файлов прямо на карте — привычка, которая однажды оборвёт запись на середине из-за кэша операционки. Windows кэширует запись на диск по умолчанию, и значок «можно вынимать» возникает раньше, чем байты доезжают; зелёная галочка в трее — не бюрократия, а команда сброса кэша. Для чтения риск невелик, но привычка «копия сначала» спасала мне архив чаще, чем любая утилита восстановления.
Контроль собранности файла до графика — три взгляда. Число строк в CSV против счётчика строк в Serial: расхождение показывает, сколько строк потерялось по дороге, а не испортилось в Excel. Первая и последняя строка как рамки: штампы на границах говорят, какую именно неделю вы открыли, без прокрутки ста тысяч строк. И перевод строки в конце файла: последняя строка без него — верный признак оборванной записи, хвост стоит перепроверить. На всё про всё уходит минута, а график строится уже по проверенному файлу.
В модели перещёлкайте формат при одном и том же значении pot: строка CSV платит символами и переводом строки, байтовый вид стоит ровно четыре. На реальной плате после пары минут работы у вас на карте будут оба файла из скетча — откройте log.bin в HxD и найдите в дампе числа из Serial: смещение 0000, пара байтов счётчика, пара байтов температуры. Дальше ридер на Python из соседнего блока разворачивает дамп обратно в колонки — это и есть обратная сторона экспорта, замкнутый круг без единой ручной правки.
Граница метода: если лог будут читать люди, CSV окончателен — мастер импорта снимает все три ловушки за минуту. Байтовый путь берут, когда файл копится годами на крошечной карте, едет по медленному каналу или разбирается программой. И ещё одна роскошь фиксированной длины, которой у CSV нет: прямой доступ к N-й записи через seek — ридер может вытянуть запись номер сто тысяч, не читая ни одной предыдущей.
| Инструмент | Формат | Когда брать |
|---|---|---|
| Excel, мастер импорта | CSV | графики и фильтры руками; три настройки при каждом открытии |
| LibreOffice, Google Sheets | CSV | быстрый просмотр на чужом компьютере, часто угадывает сам |
| HxD, xxd | бинарник | диагноз формата: смещения, пары байтов, ASCII-колонка |
| Python и struct | бинарник | массовое чтение дампа в колонки за четыре строки |
- 1. Excel открыл CSV двойным кликом, столбец температур не строит график. Первое подозрение?
- 2. В дампе на нулевом смещении видна пара 0C 00, счётчик первой записи — 12. Что это значит?
- 3. Сколько байтов занимает запись из счётчика uint16 и температуры int16?
- 4. N-я запись байтового файла из двух полей лежит по смещению...
- 5. Шапку CSV пишем...
- Мастер импорта: разделитель запятая, десятичный знак точка — иначе числа прилетают текстом.
- Шапка латиницей переживает любую локаль: скетч пишет UTF-8, Excel без подсказки читает Windows-1251.
- Hex-просмотр показывает смещение, байты парами и ASCII-колонку; знакомое число в дампе — признак живого формата.
- Запись из двух полей стоит 4 байта против 9 у строки CSV; N-я запись лежит по смещению 4 на N.
- Ридер на Python со struct разворачивает дамп обратно в колонки за четыре строки.
Частые вопросы по теме урока
- Excel открыл CSV двойным кликом, столбец температур не строит график. Первое подозрение?
- Скетч печатает точку, русская локаль ждёт запятую — числа прилетают текстом. Мастер импорта с явным десятичным разделителем собирает файл заново за три клика. Региональные настройки: точка в файле против запятой в локали
- В дампе на нулевом смещении видна пара 0C 00, счётчик первой записи — 12. Что это значит?
- AVR раскладывает многобайтовые числа младшим байтом вперёд, поэтому 12 выглядит как 0C 00. Найденное знакомое значение на ожидаемом смещении — признак живого формата. Это 12 в little-endian: младший байт первым, формат на месте
- Сколько байтов занимает запись из счётчика uint16 и температуры int16?
- Два поля по два байта — итого четыре, против девяти у текстовой строки с переводом строки. Экономия в два с лишним раза растёт с длиной полей. 4 — по два байта на поле
- N-я запись байтового файла из двух полей лежит по смещению...
- Фиксированная длина записи даёт прямой доступ: смещение равно 4 умножить на N, и seek доставляет запись без чтения всего файла. 4 на N — по длине записи
- Шапку CSV пишем...
- Скетч пишет UTF-8, Excel без маркера читает Windows-1251 — русская шапка превращается в кракозябры. Латиница стоит нуля усилий и снимает вопрос целиком. Латиницей: переживёт любую локаль и кодировку