Как система превращает показания весов и видеопоток в ответ на вопрос «сколько корма реально съели коровы и где потерялось остальное».
Корм проходит четыре точки, и в каждой измеряется своим способом с разной точностью. Вся система построена вокруг того, чтобы эти четыре измерения свести и объяснить разницу между ними.
| Точка | Что измеряется | Как | Точность | Тип данных |
|---|---|---|---|---|
| Весовая | Брутто на въезде, тара на выезде | Индикатор весов по RS-232 + камера номера | ±20 кг | измерено |
| Склад | Факт и длительность выгрузки, какой силос | Камера зоны, детекция техники | событие | измерено |
| Измельчитель | Вес партии, время работы, рацион | Камера на табло, распознавание цифр | ±1 цифра | распознано |
| Кормовой стол | Сколько корма и когда получил каждый лок | Камеры над локами, подсчёт ковшей | ±10 % | оценено |
source и confidence, а в интерфейсе — пометка источника.
Всё крутится на одном локальном сервере с GPU. Интернет на ферме ненадёжен, поэтому облако — только опциональная выгрузка отчётов, а не место, где живут данные.
Коллекторы падают и перезапускаются — камера отвалилась, кабель дёрнули. Шина развязывает их от ядра: события копятся и доходят позже, ничего не теряется.
14 камер, из них постоянного инференса требуют 3–4. Остальные работают по событию (техника в кадре). Одной GPU уровня RTX 4060 достаточно с запасом.
Учёт не должен останавливаться из-за интернета. Локальный сервер — источник истины, наружу уходят только отчёты и тревоги.
База делится на два уровня, и это единственное архитектурное решение, которое нельзя менять по ходу дела.
Каждое взвешивание, каждое распознавание номера, каждый ковш — отдельная строка с меткой времени, кадром и уверенностью. Эти таблицы не редактируются никогда, даже оператором. Это доказательная база.
Рейсы, партии, кормления, суточный баланс. Если алгоритм сшивания улучшили — производные пересобираются из сырых событий заново, история не теряется.
corrections: что именно, старое значение, новое, причина, кто и когда.
Производная сущность пересчитывается с учётом поправки, а исходное показание остаётся нетронутым.
Без этого система бесполезна в споре «весы врут» — а именно ради таких споров её и ставят.
Из этого следует ещё одно правило: любая производная цифра хранит ссылки на источники.
У рейса есть gross_weighing_id и tare_weighing_id — из карточки рейса
можно провалиться в конкретное показание весов и увидеть кадр табло в момент измерения.
PostgreSQL. Сырые события — гипертаблицы TimescaleDB (партиционирование по времени, автосжатие старых данных).
Ниже — только значимые поля; служебные created_at, updated_at опущены.
| Поле | Тип | Смысл |
|---|---|---|
| id | uuid pk | |
| plate | text unique | Номер в нормализованном виде, без пробелов и регистра |
| reference_tare_kg | int | Эталонная тара. Расхождение с ней на выезде — сигнал, что в кузове что-то осталось |
| carrier | text | Перевозчик или «свой» |
| is_known | bool | false — номер увиден впервые, машина заведена автоматически |
| Поле | Тип | Смысл |
|---|---|---|
| id | int pk | |
| name | text | «Лок №7» |
| head_count | int | Голов сейчас. Меняется — ведём историю отдельной таблицей |
| ration_id | fk → rations | Какой рацион положен этой группе |
| kg_per_head | numeric | Норма на голову в сутки — из неё считается план |
| schedule | jsonb | Плановое время кормлений, например ["06:30","14:00"] |
| camera_zone_id | fk → devices | Какая камера смотрит на этот лок |
| Поле | Тип | Смысл |
|---|---|---|
| id | uuid pk | |
| kind | enum | truck_scale · mill_scale · anpr_cam · pen_cam · yard_cam |
| driver | enum | rs232 · ocr · manual — меняется в настройках, не в коде |
| connection | jsonb | Хост, порт, RTSP-строка, параметры протокола |
| accuracy_kg | int | Паспортная погрешность — попадает в отчёт |
| last_seen_at | timestamptz | Heartbeat. Молчит 5 минут — тревога |
| Поле | Тип | Смысл |
|---|---|---|
| id | bigserial pk | |
| device_id | fk → devices | Какие весы |
| weight_kg | numeric | Стабилизированное значение, не мгновенное |
| measured_at | timestamptz | Момент выхода на плато |
| source | enum | rs232 · ocr · manual |
| confidence | numeric | 1.0 для RS-232, реальная уверенность для OCR |
| frame_path | text | Кадр табло в момент измерения — доказательство |
| raw_payload | jsonb | Исходная строка индикатора как пришла. Позволит переразобрать протокол задним числом |
| Поле | Тип | Смысл |
|---|---|---|
| id | bigserial pk | |
| device_id | fk → devices | Камера въезда или выезда |
| plate_text | text | Как прочиталось, до сопоставления со справочником |
| confidence | numeric | Ниже 0.85 — строка помечается на проверку |
| direction | enum | in · out |
| captured_at | timestamptz | |
| frame_path | text | Кадр с рамкой вокруг номера |
| Поле | Тип | Смысл |
|---|---|---|
| id | bigserial pk | |
| device_id | fk → devices | Камера лока |
| pen_id | fk → pens | Лок, определённый по зоне камеры |
| machine_id | text | Какая техника — если различаем |
| detected_at | timestamptz | Момент опрокидывания ковша |
| confidence | numeric | Уверенность детектора |
| clip_path | text | 3-секундный клип — для разбора спорных случаев |
| Поле | Тип | Смысл |
|---|---|---|
| id | uuid pk | |
| vehicle_id | fk → vehicles | |
| entered_at / exited_at | timestamptz | Выезд пуст, пока рейс открыт |
| gross_kg / tare_kg | numeric | Оба значения — ссылки на конкретные взвешивания |
| net_kg | numeric generated | gross − tare. Вычисляемое поле, руками не пишется |
| gross_weighing_id | fk → raw_weighings | Провалиться к исходному показанию и кадру |
| tare_weighing_id | fk → raw_weighings | |
| in_plate_read_id | fk → raw_plate_reads | Фото номера на въезде |
| out_plate_read_id | fk → raw_plate_reads | Фото номера на выезде — сверка, что уехала та же машина |
| storage_id | fk → storages | Куда выгрузили — с камеры склада |
| status | enum | open · closed · anomaly · manual |
| anomaly_flags | text[] | Что не так: tare_mismatch, no_exit, plate_mismatch |
| Поле | Тип | Смысл |
|---|---|---|
| id | uuid pk | |
| started_at / ended_at | timestamptz | Границы партии по показаниям табло |
| weight_kg | numeric | Итоговый вес партии |
| weighing_id | fk → raw_weighings | Финальное показание, из которого взят вес |
| ration_id | fk → rations | Для какой группы готовили |
| ocr_confidence | numeric | Средняя за партию |
| review_status | enum | auto · needs_review · confirmed |
| confirmed_by / at | fk, timestamptz | Кто подтвердил спорное распознавание |
| Поле | Тип | Смысл |
|---|---|---|
| id | uuid pk | |
| pen_id | fk → pens | |
| batch_id | fk → mill_batches | Из какой партии кормили — по времени |
| started_at / ended_at | timestamptz | Первый и последний ковш |
| bucket_count | int | Сколько ковшей насчитали камеры |
| estimated_kg | numeric | bucket_count × bucket_capacity_kg |
| planned_kg | numeric | Снимок плана на момент кормления — план может меняться |
| deviation_pct | numeric generated | Отклонение от плана. По нему срабатывают тревоги |
| delay_min | int | Опоздание от графика лока |
| Поле | Тип | Смысл |
|---|---|---|
| date | date pk | |
| brought_kg | numeric | Сумма net_kg закрытых рейсов |
| milled_kg | numeric | Сумма партий |
| delivered_kg | numeric | Сумма кормлений |
| planned_kg | numeric | Сумма планов по локам |
| explained_loss_kg | numeric | Недокорм и пропуски локов — объяснимая часть разрыва |
| unexplained_kg | numeric | Главная цифра отчёта. Всё, что не объясняется |
| Поле | Тип | Смысл |
|---|---|---|
| entity_type / entity_id | text, uuid | Что правим |
| field | text | Какое поле |
| old_value / new_value | jsonb | Было и стало |
| reason | text | Обязательно. Без причины правка не сохраняется |
| author_id | fk → users | Кто |
| created_at | timestamptz | Когда |
Также: alerts (тревоги и подтверждения), storages (силосы),
rations (рационы), users, pen_head_history (история поголовья по локам).
Самая нетривиальная часть системы. Никто не нажимает кнопку «начать рейс» — система сама понимает, что четыре разрозненных события относятся к одной машине.
Индикатор шлёт значение непрерывно, и в момент заезда оно скачет. Брать первое пришедшее нельзя. Коллектор ждёт, пока показание не перестанет меняться больше чем на 10 кг в течение 3 секунд — и только тогда публикует событие. То же для OCR: пока цифры на табло «пляшут», партия не фиксируется.
plateau(window=3s, tolerance=10kg) → publish weighing
OCR путает похожие символы — 0 и O, B и 8.
Прочитанный текст нормализуется и ищется в справочнике с допуском в один символ.
Не нашлось — машина заводится автоматически с флагом is_known = false,
оператор потом решает, новая это машина или ошибка распознавания.
Взвешивание и распознавание номера считаются одним событием, если они пришли с устройств
одной точки в пределах 10 секунд. Дальше: direction = in открывает рейс,
direction = out ищет открытый рейс той же машины и закрывает его.
Если открытых рейсов у машины два — это аномалия, оба уходят на разбор.
Рейс проходит через конечный набор состояний. Ни одно из них не является тупиком без оператора — любая аномалия либо разрешается автоматически, либо ждёт человека, но не исчезает.
| Флаг | Когда ставится | Что делает система |
|---|---|---|
| no_exit | Машина въехала, но не выехала за 6 часов | Тревога оператору, рейс не идёт в баланс |
| tare_mismatch | Тара отличается от эталонной больше чем на 200 кг | Тревога: возможно, часть груза осталась в кузове |
| plate_mismatch | На выезде распознан другой номер | Оба кадра рядом, оператор подтверждает или разделяет рейсы |
| low_confidence | Уверенность распознавания ниже 0,85 | Запись помечается, но учитывается — оператор проверяет постфактум |
| double_open | У машины два открытых рейса | Оба на разбор — обычно значит пропущенный выезд |
| device_silent | Устройство молчит больше 5 минут | Тревога технику, в отчёте — пометка о неполноте данных за период |
Здесь нет события «начать партию» — его надо вывести из показаний табло. Логика такая же по духу, как со стабилизацией веса, но на другом масштабе времени.
raw_weighings с уверенностью каждого кадра.review_status = needs_review и всплывает у оператора с кадрами табло.Самое грубое измерение в цепочке и единственное, где счёт идёт не в килограммах, а в событиях.
raw_bucket_events с трёхсекундным клипом.bucket_count × bucket_capacity_kg. Ёмкость ковша калибруется в первые недели: сравниваем сумму по локам с весом партии измельчителя и подгоняем коэффициент.pens.kg_per_head × head_count на момент кормления.devices.
В 23:59 фоновая задача собирает суточный баланс. Это единственное место, где три измерения встречаются в одной формуле — и главная ценность всей системы.
-- 1. что реально заехало (только закрытые рейсы) brought = Σ truck_visits.net_kg WHERE status IN ('closed', 'closed_manual') -- 2. что приготовили milled = Σ mill_batches.weight_kg -- 3. что вынесли на кормовой стол delivered = Σ feedings.estimated_kg planned = Σ feedings.planned_kg -- 4. объяснимая часть разрыва: пропуски и недокормы explained = Σ (planned_kg − estimated_kg) WHERE deviation_pct < -15 -- 5. главная цифра отчёта unexplained = milled − delivered − explained
На примере из прототипа: измельчили 67,90 т, вынесли 59,11 т, разрыв 8,79 т. Из него 7,65 т — это непокормленный лок №12 и недокорм лока №7. Остаётся 1,14 т необъяснённого — при погрешности оценки ковшей ±10 % это шум, а не пропажа.
unexplained — почти всегда сбой
распознавания. Неделя подряд — уже разговор с людьми. Именно поэтому цифра хранится по дням и выводится
трендом, а не разовым числом: смысл появляется в динамике.
| Слой | Чем делаем | Почему |
|---|---|---|
| Сбор с камер | Python, GStreamer / OpenCV, RTSP | Стандарт для видеопотоков, стабильно держит переподключения |
| Модели CV | YOLO для детекции, отдельная модель под семисегментное табло, ANPR-модель под местные номера | Готовые ANPR плохо работают на казахстанских номерах — нужен дообученный вариант |
| Чтение весов | pyserial поверх Ethernet-serial конвертера | Конвертер вместо длинного COM-кабеля — меньше точек отказа |
| Шина | MQTT (Mosquitto) | Лёгкий, переживает обрывы, буферизует на диск |
| Ядро и API | Python, FastAPI, SQLAlchemy, Alembic | Тот же язык, что и CV — одна команда, один стек |
| База | PostgreSQL 16 + TimescaleDB | Сырые события — это временной ряд: автопартиционирование и сжатие из коробки |
| Кадры | Локальный диск или MinIO, ретенция 90 дней | Кадры тяжёлые; спорные записи хранятся дольше по флагу |
| Интерфейс | Веб, тот что в прототипе | Работает с любого устройства в локальной сети, ничего не устанавливать |
| Оповещения | Telegram Bot API | Все уже в Telegram, отдельное приложение никто ставить не будет |
| Развёртывание | Docker Compose на одном сервере | Обновление и откат одной командой, без администратора на ферме |
GPU уровня RTX 4060 (12 ГБ), 32 ГБ ОЗУ, 2 ТБ SSD под кадры, ИБП обязательно. Ставится в помещение с розеткой и хоть каким-то охлаждением.
Въезд и выезд — с ИК-подсветкой и коротким затвором, иначе ночью номер смажет. Табло измельчителя — жёстко зафиксированная, с блендой от бликов. Локи — обзорные, без особых требований.
PoE-коммутаторы, витая пара до камер. Wi-Fi для видеопотоков не годится — будут разрывы и потерянные события.
Ethernet-serial конвертер у индикатора (~50–100 $) или мини-ПК в будке. Нужна розетка 220 В рядом с индикатором.
Всё выше спроектировано так, чтобы пережить любой ответ на эти вопросы. Но пока ответов нет, часть цифр в смете будет с диапазоном.
| Вопрос | На что влияет | Как страхуемся |
|---|---|---|
| Марка и протокол индикатора весов | Сроки на драйвер: известный протокол — день, экзотика — неделя | Сырая строка индикатора пишется в raw_payload, протокол можно переразобрать задним числом |
| Тип табло измельчителя | Семисегментное LED — решаемо; стрелочное — придётся ставить весы отдельно | Драйвер узла меняется в настройках, ядро не трогается |
| Направление корма | Знак в формуле нетто | Хранятся оба веса, знак вычисляется при отчёте |
| Реальное число локов и голов | Количество камер и стоимость железа | Локи — строки в справочнике, добавляются без разработки |
| Ёмкость ковша экскаватора | Точность оценки раздачи | Коэффициент калибруется в первые 2 недели по сверке с партиями измельчителя |
| Главная боль заказчика | Куда вкладывать 80 % усилий: в доказательность или в аналитику | Ядро одинаковое; различается объём работ по фотофиксации и отчётам |