198б. Мини-проект: USB-самописец и программа на ПК
Прибор этого проекта — самописец: F407 меряет два канала АЦП тысячу раз в секунду и непрерывно шлёт поток на компьютер, где программа принимает, проверяет и пишет файл с графиком. Компьютеру прибор виден обычным последовательным портом — класс CDC из раздела 20. Новое здесь не USB (его механика уже разобрана), а всё, что между измерением и файлом: двоичный кадр с контролем целостности, очередь на случай пауз приёмника и программа на ПК, которая честно считает потери.
QEMU здесь не помощник: USB OTG он не моделирует — ни перечисления,
ни конечных точек. Поэтому проект разрезан по шву, проверяемому без
платы (приём разделов 184 и 196): кадр и очередь — общий код logger.c
без единого регистра, собранный и для Cortex-M4, и обычным gcc;
поток кадров генератор гонит через канал в приёмник. Файлы —
_verify/usb198b/: logger.h, logger.c, sim_queue.c,
gen_frames.py, logger_rx.py; прогоны записаны в
_verify/usb198b/*_host.txt, сверяет _verify/usb198b_check.py.
Как это называется в документации
Оригинал Перевод data logger самописец: прибор, пишущий поток измерений payload полезная нагрузка: данные кадра без заголовка и CRC magic number сигнатура: приметные байты начала кадра sequence number порядковый номер кадра: по дырам в нём видны потери throughput пропускная способность: сколько байтов в секунду проходит goodput полезная её часть — без заголовков, CRC и повторов drop-oldest / drop-newest вытеснять старое / отбрасывать новое при переполнении
198б.1 Что делает прибор и кто за что отвечает
| Функция | Как |
|---|---|
| мерить | два канала АЦП (раздел 14), 1 кГц, время — от SysTick, 1 мс |
| упаковывать | кадр 20 байт: сигнатура, номер, время, каналы, счётчик вытеснений, CRC-32 |
| переживать паузы ПК | очередь на 32 кадра; полна — новое вытесняет старое |
| отдавать | класс CDC (рецепт 20.7): пакеты до 64 байт, хост забирает сам |
| принимать на ПК | Python: поиск сигнатуры → CRC → номера → CSV и график |
| считать потери | дыры в номерах сверяются со счётчиком вытеснений из кадров |
Два решения определяют всю архитектуру. Время метит прошивка: кадры
доезжают до программы пачками и с задержкой — хост опрашивает устройство
по своему расписанию (раздел 20), приёмник читает порт, когда его пустит
система. Поэтому в каждом кадре лежит t_ms от SysTick, и графики
строятся только по нему, а не по часам ПК. И буферизует прошивка:
обратного канала нет, попросить «повтори» некому — паузу приёмника
переживает только очередь в ОЗУ прибора (198б.3).
Конечных точек хватает с запасом: CDC требует три сверх нулевой —
массовые IN и OUT для данных и прерывающую IN для уведомлений. OTG FS
в F407 даёт четыре IN и четыре OUT, включая нулевую (заголовок
stm32f407xx.h из пака: USB_OTG_FS_MAX_IN_ENDPOINTS 4). Буферы
точек — общие 1280 байт FIFO (USB_OTG_FS_TOTAL_FIFO_SIZE), то есть
320 слов, которые прошивка делит между приёмным FIFO и передающими;
кадрам по 20 байт хватает любой разумной разбивки.
198б.2 Кадр: двадцать байт, байт за байтом
Текстовый формат («t=1000 ch0=-123…») был бы вдвое-втрое толще и требовал
разбора строк; двоичный кадр фиксированного размера проще и прибору,
и программе. Поля — младшим байтом вперёд: так их пишет в память
Cortex-M, и ПК почти всюду читает так же (порядок байтов — раздел 3).
Сборка кадра в logger.c — буквально байт за байтом, без структур
с выравниванием: формат виден глазами и не зависит от компилятора.
| Байты | Поле | Зачем |
|---|---|---|
| 0–1 | сигнатура 0xA5 0x5A |
найти начало кадра в потоке без границ |
| 2 | тип 0x01 |
задел: калибровка, журнал, ответы — другие типы |
| 3 | флаги | бит 0: перед этим кадром очередь вытесняла |
| 4–5 | порядковый номер | дыры в номерах = потерянные кадры |
| 6–9 | t_ms |
время измерения по часам прибора |
| 10–11, 12–13 | канал 0, канал 1 | сырые знаковые коды АЦП, без пересчёта |
| 14–15 | вытеснено всего | младшие 16 бит счётчика вытеснений |
| 16–19 | CRC-32 | по первым 16 байтам — ровно 4 слова |
Счётчик вытеснений — сквозной, а не «с прошлого кадра»: вытесненный кадр уносит своё поле с собой, и раздельные счётчики потерялись бы вместе с ним. Разность сквозного счётчика в первом и последнем принятых кадрах всегда говорит, сколько вытеснила очередь, — сколько бы кадров между ними ни пропало.
CRC замыкает первые 16 байт — ровно четыре слова, как их ест аппаратный блок STM32 (раздел 43): слова из памяти, без переворота байтов. На компьютере то же считает десяток строк:
uint32_t lg_crc32_words(const uint8_t *p, uint32_t n)
{
uint32_t crc = 0xFFFFFFFFu;
for (uint32_t i = 0; i < n; i += 4) {
/* слово так, как его прочитал бы процессор из памяти (младшим вперёд) */
crc ^= (uint32_t)p[i] | (uint32_t)p[i + 1] << 8 |
(uint32_t)p[i + 2] << 16 | (uint32_t)p[i + 3] << 24;
for (int b = 0; b < 32; b++)
crc = (crc & 0x80000000u) ? (crc << 1) ^ 0x04C11DB7u : crc << 1;
}
return crc;
}
⚠️ Сверьте реализацию по числам раздела 43, прежде чем гонять кадры. Модель печатает три контрольных значения: по байтам от
123456789выходит0x0376E6E7(CRC-32/MPEG-2), по словам от12345678—0xFEFC54F9, и это же число даёт побайтовый счёт строки43218765— порядок байтов в слове перевёрнут, как и описано там. Совпали все три — прошивка может писатьCRC->DRбез__REV, а ПК считать этой функцией.
Счётчики намеренно узкие. Номер кадра — 16 бит: оборот через 65 536 кадров, при 1 кГц — каждые 65,5 с; приёмнику хватает, потому что дыры он считает разностью по модулю 2¹⁶. Время — 32 бита миллисекунд: оборот через 49,7 суток непрерывной записи.
198б.3 Очередь: вытеснять старое или стоять
Очередь — кольцо на 32 кадра (640 байт буферов; вся структура со счётчиками — 652 байта, напечатано моделью). При 1 кГц это подушка на 32 мс молчания приёмника — обычные паузы планировщика и записи на диск она съедает молча. Интересное начинается, когда пауза длиннее. Вариантов два, и оба теряют данные — вопрос лишь, какие:
- «стоп» (drop-newest): очередь полна — новый кадр отбрасывается. Выживает начало паузы, теряется её конец — самое свежее;
- «новое вытесняет старое» (drop-oldest): полна — самый старый кадр выбрасывается, новый ложится. Выживает свежее.
В ядре обе политики рядом (lg_push и lg_push_stop), чтобы модель
могла их столкнуть. Вытеснение — три строки в начале lg_push; заодно
взводится флаг и растёт сквозной счётчик:
void lg_push(struct lg_queue *q, uint32_t t_ms, int16_t ch0, int16_t ch1)
{
uint8_t evicted = 0;
if (q->count == LG_DEPTH) { /* вытеснить самый старый */
q->head = (uint16_t)((q->head + 1) % LG_DEPTH);
q->count--;
q->drops_total++;
evicted = 1;
}
uint16_t tail = (uint16_t)((q->head + q->count) % LG_DEPTH);
lg_build(q, q->buf[tail], t_ms, ch0, ch1, evicted);
q->count++;
}
При «стопе» есть тонкость: кадр не создан, но номер расходуется — иначе приёмник не увидел бы дыру, и потеря прошла бы незамеченной.
Модель sim_queue.c прогоняет 10 000 мс: производитель кладёт кадр
каждую миллисекунду, потребитель забирает до четырёх кадров за
миллисекунду, но на [3000; 3250) мс замолкает — четверть секунды хост
занят чем-то своим. Обе политики, один и тот же прогон:
| Политика | Дошло | Потеряно | Потерянные номера |
|---|---|---|---|
| новое вытесняет старое | 9781 | 219 | 2999–3217 — начало паузы |
| «стоп» | 9781 | 219 | 3031–3249 — конец паузы |
Потерь поровну — за паузу родилось больше кадров, чем влезает, и лишние
погибли при любой политике. Различие в том, какие выжили: после
«стопа» запись обрывается на номере 3030 — событие, из-за которого хост
завис, в журнале отсутствует; при вытеснении дыра лежит в начале паузы,
а последние 250 мс — на месте. Самописцу почти всегда нужно второе.
И заметьте: потеряно 219, а не 218 кадров — очередь встретила паузу
не пустой (кадр 2999 ещё ждал отправки), свободных мест было 31.
Бухгалтерия сходится: сумма дыр в номерах принятых кадров равна
сквозному счётчику в последнем — 219 и там и там, account=ok.
Ядро собрано и для прошивки: arm-none-eabi-gcc -mcpu=cortex-m4 -Os
даёт 504 байта кода на весь logger.c — сборка кадра, CRC и обе
политики; своих данных у ядра нет, очередь живёт у владельца.
На плате lg_push зовётся из прерывания АЦП, а главный цикл по
lg_pop скармливает кадры CDC_Transmit_FS (рецепт 20.7): пока класс
отвечает «занят», следующий кадр просто не забирается из очереди.
198б.4 🔧 Приёмник на ПК: сигнатура, CRC, счёт потерь
Со стороны компьютера прибор — последовательный порт, читать его
из Python умеет pyserial: serial.Serial("COM5") и port.read(4096)
в цикле. Всё интересное — в разборе прочитанного.
⚠️ Порт отдаёт куски произвольного размера. USB возит пакетами по 64 байта, система склеивает их в буфер,
readвозвращает что накопилось: полтора кадра, треть кадра, десять кадров. Границ кадров не существует нигде, кроме самого формата — поэтому разбор обязан быть потоковым: принял кусок, добавил к хвосту, вынул целые кадры, остаток оставил до следующего куска.
Разбор — метод feed класса Parser в logger_rx.py, ему всё равно,
откуда пришёл кусок — из порта, файла или канала:
def feed(self, chunk):
self.buf += chunk
while True:
i = self.buf.find(MAGIC)
if i < 0: # сигнатуры нет — всё, кроме
keep = max(0, len(self.buf) - 1) # последнего байта, мусор
self.bytes_skipped += keep
self.buf = self.buf[keep:]
return
self.bytes_skipped += i
self.buf = self.buf[i:]
if len(self.buf) < FRAME: # кадр ещё не дошёл целиком
return
cand = self.buf[:FRAME]
if crc32_words(cand[:16]) == struct.unpack("<I", cand[16:])[0]:
self._accept(cand)
self.buf = self.buf[FRAME:]
else: # ложная сигнатура или порча
self.crc_errors += 1
self.bytes_skipped += 1
self.buf = self.buf[1:]
Порядок обязателен: сигнатура лишь кандидат на кадр, подтверждает
его CRC. Два байта 0xA5 0x5A случаются и внутри полезной нагрузки,
и в мусоре; CRC ложного кандидата не сойдётся, разбор сдвинется
на один байт и продолжит поиск — это и есть восстановление после сбоя.
Хвост короче кадра не трогается: остальное дошлёт следующий read.
Принятые кадры считаются по трём графам: frames — сколько дошло,
lost_by_seq — сумма дыр в номерах, drops_accounted — разность
сквозного счётчика вытеснений между последним и первым кадром. Разница
lost_by_seq − drops_accounted — потери не по вине очереди: порча
на линии, ошибки разбора. CSV пишется колонками t_ms,ch0,ch1 —
график строится чем угодно, от табличного редактора до matplotlib.
198б.5 Пропускная способность: потолок и запасы
Сколько вообще может провезти CDC на полной скорости? Считается честно, из кадра шины (не путать с кадром самописца): при 12 Мбит/с за миллисекундный кадр шины проходит 1500 «сырых» байт. Массовая транзакция с полем данных 64 байта стоит 77 байт: 13 байт накладных — по байту SYNC и PID на каждый из трёх пакетов (маркерный, данные, подтверждение), 2 байта адреса с CRC5, 2 байта CRC16 поля данных и около трёх байт межпакетных промежутков. В кадр шины помещается 1500 / 77 = 19 транзакций — потолок массовых передач:
19 × 64 = 1216 байт за миллисекунду ≈ 1,2 МБ/с.
Это потолок всей шины: он делится между устройствами на том же контроллере и урезается другим трафиком; живой стек с ним и не сравняется. Но масштаб виден сразу:
| Поток | Байт/с | Хватает? |
|---|---|---|
| самописец: 1 кГц × 20 байт | 20 000 | запас 60,8 раза |
| потолок CDC FS | 1 216 000 | — |
| UART 115 200 бод, 8N1 | 11 520 | нет: меньше потока |
| UART 230 400 бод, 8N1 | 23 040 | да, запас 15,2 % |
Отсюда два вывода. Первый: на этих скоростях USB даже не вспотел — тесно станет на десятках килогерц. Второй: запасной канал UART (раздел 16) на привычных 115 200 бод наш поток не тянет — байт на проводе стоит 10 бит, и 11 520 Б/с меньше 20 000; нужен либо вдвое быстрый порт, либо прореживание. Полезной нагрузки в кадре при этом 4 байта из 20 — накладные 80 %; что с этим делать — в 198б.7.
198б.6 Прогон без платы: генератор гонит поток в приёмник
gen_frames.py собирает те же кадры, что logger.c — модель
напечатала опорный кадр A5 5A 01 00 00 00 E8 03 00 00 85 FF C8 01 00
00 90 E0 61 E8, и генератор на тех же данных обязан выдать его байт
в байт (проверка сверяет), — и через канал кормит logger_rx.py,
как это делал бы порт.
Сценарий первый — честный канал, 10 000 кадров подряд:
frames=10000 crc_errors=0 lost_by_seq=0 drops_accounted=0 lost_unexplained=0 bytes_skipped=0
Ни потерь, ни ошибок CRC, ни выброшенных байтов; CSV — 10 001 строка с заголовком.
Сценарий второй — канал с грязью: 157 байт мусора двумя вставками
(внутри одной — нарочно подброшенная ложная сигнатура A5 5A), пять
кадров «вытеснены очередью» (номера пропущены, сквозной счётчик уехал
на 5), в трёх кадрах после подсчёта CRC испорчен байт полезной нагрузки:
frames=9992 crc_errors=4 lost_by_seq=8 drops_accounted=5 lost_unexplained=3 bytes_skipped=217
Всё сходится до байта. Дошло 9992 = 10 000 − 5 вытесненных − 3 испорченных. Ошибок CRC четыре: три порчи плюс ложная сигнатура из мусора. Дыр в номерах восемь: пять объяснил счётчик вытеснений из самих кадров, три остались «на линии» — ровно испорченные. Выброшено 217 байт: 157 мусора и 60 байт трёх кадров, не прошедших CRC. Приёмник не только пережил грязь — он отчитался, какая потеря чья.
198б.7 Что улучшить
- Пачки отсчётов: класть в кадр не пару каналов, а 24 пары — кадр
112 байт, полезная доля с 20 % вырастает до 86 %. Формат готов:
другой
типкадра. - Команды прибору: канал OUT у CDC уже есть, приём строк разобран в рецепте 20.7; «start», «stop», «rate 5000».
- High Speed: второй контроллер F407 — OTG HS с FIFO 4096 байт и шестью точками IN (заголовок пака), но полной скорости HS нужна внешняя микросхема физического уровня по ULPI.
- Файл вместо порта: класс MSC (раздел 20.4) плюс карта памяти (разделы 41–42) — самописец без компьютера; очередь и кадры те же.
- Сон между отсчётами: WFI в паузах главного цикла (раздел 10), АЦП по таймеру, пробуждение по прерыванию.
198б.8 Грабли
Скорость, заданная при открытии порта, ни на что не влияет. Так и задумано: у CDC «бод» — просьба из старого мира, до проводов она не доезжает, байты летят пакетами USB. Настоящая арифметика — в 198б.5.
CRC «не сходится с компьютером». Аппаратный блок ест слова младшим
байтом вперёд (раздел 43). Либо прошивка кормит CRC->DR через __REV,
либо — как здесь — ПК считает по словам; контроль: 12345678 по словам
даёт 0xFEFC54F9.
Каждый второй кадр — «испорчен». Приёмник верит, что read вернёт
целый кадр. Не вернёт: границ кадров в порту нет, кадр режется на любом
байте. Потоковый разбор с хвостом — единственный честный путь.
Запись обрывается за мгновение до события. Политика «стоп»: пока хост молчал, очередь сохранила старые кадры и выбросила свежие — само событие. Самописцу — вытеснение старого (прогон 198б.3).
Потери растут, хотя пауз нет. Если lost_by_seq опережает
drops_accounted, очередь ни при чём — кадры гибнут по пути: кабель,
питание хаба, гонка записи кадра в прошивке. Глубина очереди не поможет.
Порт «занят» или прибор «не определяется». Порт держит забытый терминал — CDC монопольный; а «не определяется» на самодельной плате — почти всегда тактирование и обвязка из раздела 20, не этот проект.
198б.9 Итоги
- Кадр — 20 байт: сигнатура
0xA5 0x5A, тип, флаги, номер,t_ms, два канала, сквозной счётчик вытеснений и CRC-32 по первым 16 байтам — ровно четырём словам аппаратного блока. - Время метит прошивка, буферизует прошивка: очередь 32 кадра (652 байта с буферами и счётчиками) гасит паузу приёмника до 32 мс.
- Пауза 250 мс стоит 219 кадров при любой политике; выбор политики решает, какие выжили: «стоп» теряет конец паузы, вытеснение — начало. Самописцу — вытеснять старое.
- Потолок массовых передач полной скорости — 19 транзакций по 64 байта на кадр шины: 1 216 000 Б/с. Поток 1 кГц × 20 байт = 20 000 Б/с — запас 60,8 раза; UART 115 200 такой поток не тянет (11 520 Б/с), 230 400 — тянет с запасом 15,2 %.
- Приёмник: сигнатура — кандидат, подтверждает CRC; 10 000 кадров через канал — ноль потерь и ошибок. С грязью: дыр 8 = 5 вытеснено (по счётчику из кадров) + 3 испорчено; выброшено 217 байт.
- Ядро самописца на Cortex-M4 — 504 байта кода; контроль CRC:
0x0376E6E7по байтам от123456789,0xFEFC54F9по словам от12345678.
Полные исходники
logger.c — сборка кадра и очередь.
/* logger.c — сборка кадра и очередь. Собирается arm-none-eabi-gcc для
* Cortex-M4 (прошивка) и обычным gcc (модель на компьютере) без правок. */
#include "logger.h"
uint32_t lg_crc32_words(const uint8_t *p, uint32_t n)
{
uint32_t crc = 0xFFFFFFFFu;
for (uint32_t i = 0; i < n; i += 4) {
/* слово так, как его прочитал бы процессор из памяти (младшим вперёд) */
crc ^= (uint32_t)p[i] | (uint32_t)p[i + 1] << 8 |
(uint32_t)p[i + 2] << 16 | (uint32_t)p[i + 3] << 24;
for (int b = 0; b < 32; b++)
crc = (crc & 0x80000000u) ? (crc << 1) ^ 0x04C11DB7u : crc << 1;
}
return crc;
}
void lg_init(struct lg_queue *q)
{
q->head = 0; q->count = 0; q->seq = 0; q->drops_total = 0;
}
/* Кадр целиком, байт за байтом; flags — бит 0: перед кадром были вытеснения */
static void lg_build(struct lg_queue *q, uint8_t *f, uint32_t t_ms,
int16_t ch0, int16_t ch1, uint8_t flags)
{
uint16_t drops16 = (uint16_t)q->drops_total; /* младшие 16 бит */
f[0] = LG_MAGIC0;
f[1] = LG_MAGIC1;
f[2] = LG_TYPE;
f[3] = flags;
f[4] = (uint8_t)(q->seq); f[5] = (uint8_t)(q->seq >> 8);
f[6] = (uint8_t)(t_ms); f[7] = (uint8_t)(t_ms >> 8);
f[8] = (uint8_t)(t_ms >> 16); f[9] = (uint8_t)(t_ms >> 24);
f[10] = (uint8_t)((uint16_t)ch0); f[11] = (uint8_t)((uint16_t)ch0 >> 8);
f[12] = (uint8_t)((uint16_t)ch1); f[13] = (uint8_t)((uint16_t)ch1 >> 8);
f[14] = (uint8_t)(drops16); f[15] = (uint8_t)(drops16 >> 8);
uint32_t crc = lg_crc32_words(f, 16);
f[16] = (uint8_t)(crc); f[17] = (uint8_t)(crc >> 8);
f[18] = (uint8_t)(crc >> 16); f[19] = (uint8_t)(crc >> 24);
q->seq++;
}
void lg_push(struct lg_queue *q, uint32_t t_ms, int16_t ch0, int16_t ch1)
{
uint8_t evicted = 0;
if (q->count == LG_DEPTH) { /* вытеснить самый старый */
q->head = (uint16_t)((q->head + 1) % LG_DEPTH);
q->count--;
q->drops_total++;
evicted = 1;
}
uint16_t tail = (uint16_t)((q->head + q->count) % LG_DEPTH);
lg_build(q, q->buf[tail], t_ms, ch0, ch1, evicted);
q->count++;
}
void lg_push_stop(struct lg_queue *q, uint32_t t_ms, int16_t ch0, int16_t ch1)
{
if (q->count == LG_DEPTH) { /* места нет — кадр пропал */
q->drops_total++;
q->seq++; /* номер расходуется: приёмник увидит дыру */
return;
}
uint16_t tail = (uint16_t)((q->head + q->count) % LG_DEPTH);
lg_build(q, q->buf[tail], t_ms, ch0, ch1, 0);
q->count++;
}
int lg_pop(struct lg_queue *q, uint8_t out[LG_FRAME])
{
if (q->count == 0)
return 0;
const uint8_t *f = q->buf[q->head];
for (uint32_t i = 0; i < LG_FRAME; i++)
out[i] = f[i];
q->head = (uint16_t)((q->head + 1) % LG_DEPTH);
q->count--;
return 1;
}
logger_rx.py — приёмник на компьютере: сигнатура, CRC, счёт потерь, CSV.
# -*- coding: utf-8 -*-
"""logger_rx.py — приёмник самописца: сигнатура -> CRC -> счёт потерь -> CSV.
python logger_rx.py [--csv файл] поток кадров ждёт на stdin
С прибором вместо stdin открывается порт (pyserial):
port = serial.Serial("COM5") # скорость у CDC ни на что не влияет
и вместо read() у файла зовётся port.read(4096) в том же цикле.
"""
import struct
import sys
FRAME = 20
MAGIC = b"\xA5\x5A"
def crc32_words(data):
"""Тот же CRC, что считает блок STM32 и logger.c: по словам, без переворота."""
crc = 0xFFFFFFFF
for i in range(0, len(data), 4):
crc ^= int.from_bytes(data[i:i + 4], "little")
for _ in range(32):
crc = ((crc << 1) ^ 0x04C11DB7 if crc & 0x80000000 else crc << 1) & 0xFFFFFFFF
return crc
class Parser:
"""Потоковый разбор: feed() принимает куски любого размера."""
def __init__(self, on_frame):
self.buf = b""
self.on_frame = on_frame
self.frames = 0
self.crc_errors = 0
self.bytes_skipped = 0
self.lost_by_seq = 0
self.prev_seq = None
self.first_field = None
self.last_field = 0
def feed(self, chunk):
self.buf += chunk
while True:
i = self.buf.find(MAGIC)
if i < 0: # сигнатуры нет — всё, кроме
keep = max(0, len(self.buf) - 1) # последнего байта, мусор
self.bytes_skipped += keep
self.buf = self.buf[keep:]
return
self.bytes_skipped += i
self.buf = self.buf[i:]
if len(self.buf) < FRAME: # кадр ещё не дошёл целиком
return
cand = self.buf[:FRAME]
if crc32_words(cand[:16]) == struct.unpack("<I", cand[16:])[0]:
self._accept(cand)
self.buf = self.buf[FRAME:]
else: # ложная сигнатура или порча
self.crc_errors += 1
self.bytes_skipped += 1
self.buf = self.buf[1:]
def _accept(self, f):
(_, _, _, _, seq, t_ms, ch0, ch1, field) = struct.unpack("<BBBBHIhhH", f[:16])
if self.prev_seq is not None:
self.lost_by_seq += (seq - self.prev_seq - 1) & 0xFFFF
self.prev_seq = seq
if self.first_field is None:
self.first_field = field
self.last_field = field
self.frames += 1
self.on_frame(t_ms, ch0, ch1)
def report(self):
accounted = 0
if self.first_field is not None:
accounted = (self.last_field - self.first_field) & 0xFFFF
return ("frames=%d crc_errors=%d lost_by_seq=%d drops_accounted=%d "
"lost_unexplained=%d bytes_skipped=%d"
% (self.frames, self.crc_errors, self.lost_by_seq, accounted,
self.lost_by_seq - accounted, self.bytes_skipped))
def main():
csv = None
if len(sys.argv) >= 3 and sys.argv[1] == "--csv":
csv = open(sys.argv[2], "w", encoding="utf-8")
csv.write("t_ms,ch0,ch1\n")
def on_frame(t_ms, ch0, ch1):
if csv:
csv.write("%d,%d,%d\n" % (t_ms, ch0, ch1))
p = Parser(on_frame)
src = sys.stdin.buffer
while True:
chunk = src.read(4096) # порция произвольная: границ кадров нет
if not chunk:
break
p.feed(chunk)
if csv:
csv.close()
print(p.report())
if __name__ == "__main__":
main()
Дальше: раздел 198в «Мини-проект: модуль ввода-вывода на Modbus RTU» — самописец говорил в одну сторону, теперь прибор станет отвечать на запросы.