Как устроен Parallax-GPS: архитектура системы мониторинга
Платформа управления автопарком на базе GPS-трекинга, телеметрии CAN-шины и ML-аналитики.
| Стек | NestJS, TypeScript, PostgreSQL, React, MUI, Leaflet, Python (ML) |
| Архитектура | 5 микросервисов в Docker-стеке под Traefik |
| Масштаб | 35 backend-модулей, ~20 фронтенд-страниц |
О проекте
Система мониторинга транспорта - платформа управления автопарком для компаний, которые эксплуатируют коммерческую технику и хотят понимать, что с ней происходит. Система собирает данные с GPS-трекеров и CAN-шины машин, в реальном времени показывает положение и состояние парка на карте, ведёт историю перемещений, анализирует телеметрию двигателя и расход топлива, регистрирует события и отправляет уведомления в telegram, ведёт учёт ремонтов и технического обслуживания, списывает со счёта компании ежедневную оплату.
Что внутри
На карте - все машины компании в моменте. Для каждой видны актуальные параметры: скорость, направление, обороты, температура охлаждающей жидкости, напряжение бортовой сети, уровень топлива, ошибки двигателя.
История маршрутов - с очисткой треков от спуфинга, дрейфа на парковках и помех от РЭБ. Графики телеметрии - с детекцией заправок, сливов и аномалий расхода. Журнал событий - с алертами о критических состояниях машины и накопленной статистикой. Учёт техники - с привязкой ремонтов, проблем, документов и истории технического обслуживания. Биллинг - со списанием абонентской платой по тарифам, контролем остатков, защитой от двойных списаний.
Параллельно работает Python ML-сервис, который на индивидуальных моделях для каждой машины ищет аномалии - текущий контур включает потерю тяги и деградацию генератора, архитектура будет расширяться добавлением новых детекторов.
Кому нужна такая система
Компании с автопарком от 5-10 машин, которым важно не просто знать "где машина", а понимать состояние парка: кто из водителей как водит, сколько потрачено топлива, где происходили остановки, определение продолжительных простоев на холостом ходу. Решение подойдёт транспортным предприятиям, строительным компаниям с парком спецтехники, сельхозпредприятиям, коммунальным службам, корпоративным автопаркам.
Чем отличается от Wialon и подобных систем
Wialon, GPSWox и другие готовые решения - это закрытые системы со стандартным набором функций. Они хорошо подходят, если задача стандартная: показать машины на карте, посчитать пробег, выгрузить отчёт. Но как только бизнесу нужны нестандартные сценарии - индивидуальные алерты под специфику парка, собственные ML-детекторы, интеграция с внутренней CRM или 1С, специфический биллинг, - готовая система может не подойти.
Данная система с самого начала спроектирована как расширяемая платформа: открытая архитектура, документированный API, возможность писать собственные детекторы аномалий, гибкая модель тарификации, мультитенантность для обслуживания нескольких компаний с одной инсталляции. Это не альтернатива Wialon "по функциям", это альтернатива "по подходу" - для тех, кому нужна система под себя.

Стек и масштаб
Backend
Основное приложение - NestJS 11 + TypeScript 5.7, TypeORM 0.3 + PostgreSQL 15, Socket.io 4 для real-time, JWT через passport-jwt + bcrypt, PDFKit для генерации документов, dayjs/luxon для работы со временем, class-validator + class-transformer для DTO, @nestjs/schedule для cron-задач, multer для загрузок, Swagger для документации API.
TCP-серверы для трекеров - три реализации парсеров протоколов поверх Node.js TCP: Sinotrack, Galileosky, Vega/Wialon-IPS. Развёртываются как часть основного приложения на отдельных портах (9090, 9091).
ML-сервис - отдельный микросервис на Python 3 + apscheduler + pandas. Хранит обученные модели в Docker-volume.
Tile-proxy - отдельный микросервис на Node.js + sharp для хранения кеша карт и конвертации в WebP.
Frontend
React 19 + TypeScript, Redux Toolkit 2 (типизированные слайсы и селекторы), React Router 7, MUI 7 + MUI X DataGrid 8 + MUI X DatePickers 8, Leaflet 1.9 + React Leaflet 5 для карт, Recharts 3 для графиков, Socket.io-client 4.8 для real-time, axios для авторизации, react-window для виртуализации, react-dnd 16, react-pdf 10 для просмотра документов, mammoth для DOCX, xlsx для экспорта в Excel.
Архитектура - Feature-Sliced Design, разделение по бизнес-фичам на уровне директорий.
Инфраструктура и развёртывание
Docker Compose, Traefik как обратный прокси с автоматическим Let's Encrypt, разделение TCP-роутеров по протоколам трекеров, healthcheck'и для критичных сервисов, persistent volumes для БД, кеша карт и ML-моделей.
Внешние интеграции
Telegram Bot API (алерты и уведомления), Google Geolocation API (LBS-геолокация по сотовым вышкам), Perplexity AI (анализ проблем техники), OpenCellID (импорт базы вышек), OpenStreetMap + OpenTopoMap (тайлы карт).

Цифры
| Backend-модулей в NestJS | 35 |
|---|---|
| Фронтенд-страниц | 20 |
| Поддерживаемых протоколов трекеров | 3 (Sinotrack, Galileosky, Vega) |
| Сервисов в Docker-стеке | 5 (приложение, БД, ML-воркер, tile-proxy, Traefik) |
| Активных детекторов в ML-воркере | 2 (потеря тяги, исправность генератора) |
| Типов алертов реактивного мониторинга | 8 |
| Шагов в pipeline постфильтрации треков | 10 |
| Источников телеметрии в работе | GPS + CAN-шина |
Архитектура системы
Система построена как набор взаимосвязанных микросервисов, развёрнутых через Docker Compose под обратным прокси Traefik. Каждый сервис решает свою чётко очерченную задачу и общается с остальными через документированные интерфейсы - REST API, WebSocket, прямые TCP-подключения от трекеров, общая база данных для критичных сценариев.
Пять сервисов в стеке
Основное приложение (NestJS). Ядро системы. Отвечает за бизнес-логику, обработку телеметрии, биллинг, авторизацию, REST API для фронта, WebSocket для real-time обновлений, парсинг трёх протоколов трекеров через TCP-серверы на отдельных портах. Внутри - около 35 модулей, организованных по доменным областям: техника, трекеры, телеметрия, поездки, алерты, биллинг, ремонты, ТО, документы, пользователи, компании.
PostgreSQL. Основное хранилище. Все операционные данные - техника, точки треков, телеметрия, события, биллинг, документы - находятся здесь. Изоляция между компаниями обеспечена на уровне схемы через company_id во всех ключевых сущностях.
ML-воркер (Python). Отдельный микросервис для аналитики. Раз в неделю переобучает индивидуальные модели на каждую машину по накопленным данным телеметрии. Каждые 15 минут запускает детекцию аномалий по свежим данным и пишет результаты в общую БД. Сейчас работают два детектора (потеря тяги, исправность генератора), архитектура расширяется добавлением новых классов в массив активных детекторов.
Tile-proxy (Node.js). Отдельный микросервис кеширования и проксирования картографических тайлов. Принимает запросы от фронта, проверяет локальный кеш, при отсутствии обращается во внешние источники (OpenStreetMap, OpenTopoMap) с two-tier ротацией хостов и автоматическим переключением источников при ошибках. Полученные тайлы конвертируются в WebP и кешируются. Раз в сутки - очистка кеша по TTL и общему лимиту 10 ГБ.
Traefik. Обратный прокси на входе в стек. Терминирует HTTPS с автоматическим получением и обновлением сертификатов через Let's Encrypt. Маршрутизирует HTTP-запросы по правилам в основное приложение и в tile-proxy. Отдельно - TCP-роутеры на портах 9090 и 9091 для прямого подключения GPS-трекеров по их собственным протоколам.
Принципы взаимодействия
Основное приложение и БД - основной контур. Сюда стекается вся операционная нагрузка.
ML-воркер не интегрирован в основное приложение специально. Это архитектурное решение: ML-задачи имеют другие требования (Python-экосистема для pandas/scikit-learn, периодический режим работы, изоляция нагрузки от пользовательских запросов), и их разумнее держать отдельно. Воркер читает данные из общей БД и пишет туда же результаты - связь "по данным", не "по API". Это минимизирует точки сопряжения и позволяет переписать или расширить ML-часть без вмешательства в основное приложение.
Tile-proxy - изолированная вспомогательная служба. У неё нет связи ни с основным приложением, ни с БД. Её задача - только обработка картографических тайлов, и она независима от всего остального стека. Это даёт устойчивость: если у tile-proxy случается проблема (закончилось место на диске, недоступны внешние источники тайлов) - основная функциональность системы (приём данных от трекеров, реактивные алерты, биллинг, отчёты) продолжает работать без перебоев.
Трекеры подключаются напрямую, минуя HTTPS и REST. У GPS-трекеров есть собственные бинарные протоколы поверх обычного TCP, и они не понимают современную веб-обвязку. Поэтому Traefik разводит TCP-подключения по портам, и парсеры протоколов в основном приложении принимают их как есть.
Диаграммы


Ключевые технические решения
Шесть главных историй ниже - это не "обзор технологий", это конкретные инженерные задачи и решения. У каждой - своя проблема, свой подход, свои технические детали. Внутри каждой истории толстая часть свёрнута в toggle-блок "Технические детали" - раскрывать стоит тем, кто хочет глубже разобраться в реализации.
Каскадная LBS-геолокация: координаты, когда нет GPS сигнала
Многоуровневая система определения местоположения по сотовым вышкам с самообучающимся локальным кешем, fallback'ом на Google API и подстраховкой через соседние CellID - для случаев, когда GPS-приёмник не может получить координаты.
Проблема
GPS не работает всегда. Он не работает в плотной городской застройке, в подземных паркингах, в гаражах, в туннелях. Он не работает первые несколько секунд после старта приёмника - пока спутники ещё не "пойманы". Так же на него влияет работа систем РЭБ.
Современные трекеры передают помимо GPS данные о ближайших сотовых вышках - параметры LBS-фиксации (Mobile Country Code, Mobile Network Code, Local Area Code, Cell ID). По этим параметрам можно определить координаты вышки и приблизительно - координаты машины. Но координаты вышек не приходят с трекера в готовом виде. Их нужно резолвить через базы данных или внешние API, и каждый из доступных способов имеет свои ограничения по точности, стоимости, лимитам запросов.
Стандартные подходы здесь либо одноуровневые (работает только локальная база, и если в ней нет вышки - нет координат), либо завязаны на платные API с ограничениями по числу запросов. У данной системы - каскад из трёх уровней с самообучением.
Подход и решение
При получении пакета телеметрии с LBS-параметрами, запускается каскадная попытка определить местоположение. Каскад идёт от самого дешёвого и быстрого источника к самому дорогому, останавливаясь на первом, который дал результат.
Уровень 1 - локальная база OpenCellID. Это публичная база данных сотовых вышек мира (более 2 миллионов записей), которая импортирована в локальный PostgreSQL. Поиск по ней - обычный SQL-запрос с индексом, занимает миллисекунды. Если вышка есть в базе - координаты получены, готово.
Уровень 2 - Google Geolocation API. Если в локальной базе вышки нет, идём в Google API. У него есть лимит - 90 запросов в день на проект (free tier), и каждый "промах" локальной базы поедает квоту. Здесь два дополнения. Во-первых, кеширование результатов: если Google вернул координаты для какой-то вышки - мы их сохраняем в локальную базу с пометкой "получено через Google". В следующий раз эта же вышка зарезолвится локально, без обращения к API. Самообучение через selfCollect: со временем "горячие" вышки - те, мимо которых проезжают машины компании - оседают в локальной базе, и Google API дёргается только для редких, новых для нас вышек.
Уровень 3 - fallback на соседние CellID. Если Google не нашёл вышку (исчерпан лимит, ошибка сети, неизвестная вышка) - есть последний шанс. У трекеров часто приходит информация не только о текущей вышке, но и о соседних. Если хотя бы одна из них есть в нашей базе - мы возвращаем её координаты с пометкой coord_approximate=true. Это не точное местоположение машины, но соседние вышки одной соты - это километры друг от друга, не сотни километров. На карте такая точка отображается особым маркером с расходящимися волнами - пользователь видит "координаты приблизительные, GPS не было".
Каскад закрывает 95%+ случаев, когда GPS данных не хватает. Машина, которая въехала в подземный паркинг, не пропадает с карты - она перетечёт с GPS-точки на LBS-точку и продолжит быть видимой.

Двухслойная фильтрация уровня топлива: медиана плюс Калман с подтверждением событий
Двухступенчатая обработка показаний топливного датчика: медианный фильтр на 9 точек убирает мгновенные выбросы, фильтр Калмана с механизмом подтверждения событий разделяет реальные изменения от шума - без задержки реакции на настоящие заправки и сливы.
Проблема
Топливный датчик в баке - простое аналоговое устройство, и его показания постоянно прыгают. Машина проехала по кочке - топливо плеснулось, датчик показал на 5 литров больше или меньше. Машина наклонилась на повороте - то же самое. Машина завелась, поехала, остановилась - каждое такое событие вносит шум в показания.
При этом среди этого шума есть полезный сигнал: реальное изменение объёма топлива. Заправка - резкое увеличение на 30-50 литров за минуту-две. Слив (вандализм или хищение) - резкое уменьшение на десятки литров за пару минут. Постепенный расход - медленное снижение по мере движения машины.
Задача - отделить реальные изменения от мгновенных всплесков, не теряя при этом самих событий. Если просто сгладить агрессивным средним значением - настоящие заправки и сливы тоже размажутся в "плавный рост/падение", и факт события мы пропустим. Если не сглаживать вообще - каждая яма на дороге будет выглядеть как мини-слив топлива.
Подход и решение
Применена двухслойная фильтрация, в которой каждый слой решает свою часть задачи.
Слой 1 - медианный фильтр на скользящем окне в 9 точек. На вход - поток сырых показаний датчика. На выходе - медиана последних 9 значений вместо текущего "сырого". Медиана здесь работает лучше, чем среднее арифметическое: одиночный выброс на 30 литров вверх или вниз не сдвигает медиану ни на литр. Это дёшево (одно сравнение и сортировка короткого массива), снимает 90% мгновенного шума и при этом не маскирует реальные тренды - медленный плавный расход и резкие события одинаково проходят фильтр в нетронутом виде.
Слой 2 - фильтр Калмана с подтверждением событий. На вход - выход медианного фильтра. На выходе - "оценочный" уровень топлива и факт регистрации события (заправка/слив). Здесь центральная задача - различить шум от события, и решение нестандартное.
Стандартный фильтр Калмана для такой задачи имеет проблему: он "размазывает" реакцию на резкое изменение по 5-10 шагам. Реальная заправка из 40 литров будет регистрироваться как 8 литров, потом 16, потом 24, потом 32 - и только к концу выйдет на правильный уровень. Это значит, что в момент самой заправки система не знает, что она происходит - она увидит только финальный результат через несколько минут.
Поэтому был добавлен механизм подтверждения событий: фильтр следит за расхождением между предсказанным состоянием и наблюдаемым. Если расхождение превышает порог (5 литров) и сохраняется в течение 5 последовательных точек - это подтверждённое событие. Фильтр не пытается его сгладить, он его признаёт: ставит новый базовый уровень, фиксирует факт события, передаёт его в FuelEventDetector для дальнейшей классификации (заправка или слив, в зависимости от знака изменения и темпа).
Если расхождение не подтверждается через 5 точек (вернулось в норму) - это был выброс, и фильтр продолжает работать в обычном режиме сглаживания.
Это даёт чистый результат: на графике топлива у пользователя - плавные кривые расхода между событиями и резкие, чёткие "ступеньки" в местах реальных заправок и сливов. Без задержки, без размазывания, без ложных срабатываний.

Детекция заправок и сливов: разные критерии для разных событий
Стейт-машина детекции событий с топливом, где заправки и сливы определяются разными критериями (объём и устойчивость для одних, темп и скорость машины для других), с защитой от ложных срабатываний и объединение близких по времени событий - потому что эти явления физически различны.
Проблема
Когда фильтр Калмана из предыдущей главы зафиксировал событие - резкое изменение уровня топлива - это ещё не значит, что нужно сразу записать "заправка" или "слив" в журнал. Это значит "что-то произошло", и теперь надо понять что именно.
Заправка и слив - это не "одно и то же явление с разным знаком". Это физически разные процессы с разной природой и разными признаками:
- Заправка - машина стоит на АЗС, бак заливают через горловину, объём растёт за 1-3 минуты на 20-60 литров (типично) и потом стабилизируется. Машина почти всегда не движется (зажигание выключено или двигатель работает на холостых).
- Слив - это либо хищение, либо повреждение бака. Темп сильно зависит от способа: хищение - 5-10 литров в минуту, повреждение бака - может быть исчисляться считанными литрами в час. Машина в этот момент может стоять (классический случай вандализма) или ехать (повреждение топливопровода).
Если применять к ним одинаковые пороги ("любое изменение больше 5 литров") - будут постоянные ложные срабатывания. Резкие манёвры на полупустом баке - всплески показаний. Нагрев бака на солнце - медленные изменения объёма из-за теплового расширения.
Задача - выработать отдельные критерии для каждого типа события, при этом не пропустить настоящие случаи, но и не создавать ложных алертов.
Подход и решение
Реализован FuelEventDetector как стейт-машина с раздельной логикой для заправок и сливов.
Критерий заправки: прирост уровня топлива не менее 10 литров, сохраняющийся устойчиво в течение нескольких следующих точек телеметрии. Устойчивость - ключевое требование: настоящая заправка после завершения остаётся на новом уровне, и через 5-10 минут после события объём не должен внезапно "упасть обратно". Если уровень сначала вырос на 15 литров, а через 3 минуты вернулся к исходному - это не заправка, это или ошибочное показание датчика, или какой-то технический выброс. Реальная заправка фиксируется только после подтверждения устойчивости в течение 3-5 минут.
Критерий слива: уменьшение уровня не менее 3 литров за период не более 2 минут, с учётом скорости машины и состояния двигателя. Если двигатель работает и машина едет - это не слив, это нормальный расход (пусть даже большой). Если же двигатель выключен или машина стоит - резкое падение уровня уже подозрительно. Здесь критерий мягче по объёму (3 литра вместо 10).
Спайк-фильтр. Перед самой логикой детекции стоит фильтр единичных всплесков - если соседние точки нормальные, а одна резко выпадает - она игнорируется. Это снимает остаточный шум, который не поймали медианный фильтр и Калман в предыдущей проверке.
Коалесцинг сливов. Слив может прерываться, потом возобновляется через минуту-другую . Если зафиксировать каждый эпизод как отдельный - журнал будет заполнен множеством записей вместо одной. Коалесцинг объединяет последовательные сливы в течение 5 минут в одно событие с суммарным объёмом и временным диапазоном.
Двухрежимный API. Есть два сценария вызова детектора: батч - на исторических данных при импорте или пересчёте (передаётся весь массив точек, на выходе - список событий), и стрим - на live-данных, точка за точкой (детектор хранит состояние внутри и выдаёт событие сразу при срабатывании). Это позволяет использовать одну и ту же логику и для разбора прошлого, и для real-time мониторинга.
Результат - низкий процент ложных срабатываний при сохранении высокой чувствительности к настоящим случаям.
10-шаговый pipeline постфильтрации треков: GPS, спуфинг, дрейф, РЭБ
Многоступенчатая система очистки трека от физически невозможных явлений: дедупликация, проверка по одометру, маркировка спуфинга, выбраковка выбросов с rollback'ом, распознавание РЭБ-кругов, обработка дрейфа на парковках. Каждый шаг решает свою часть задачи, и эти шаги обмениваются метаданными.
Проблема
Сырой трек, который приходит с GPS-трекера - это не та траектория, по которой реально ехала машина. Это поток координат, в котором кроме реального движения присутствует:
- Дребезг GPS - машина стоит, а координаты прыгают в радиусе 50-70 метров. На карте это выглядит как ёж из коротких чёрточек вокруг точки парковки.
- Скачки в неправильную точку - иногда GPS-приёмник на одну точку даёт координаты, отличающиеся от соседних на сотни метров или километры. Физически машина не могла туда попасть и обратно за 10 секунд.
- Спуфинг - намеренная подмена сигнала GPS. Бывает злонамеренная (попытка скрыть реальный маршрут) и техническая (рядом с источниками помех координаты "уезжают" на большие расстояния).
- РЭБ-круги - характерный артефакт работы средств радиоэлектронной борьбы. Координаты начинают рисовать окружности или восьмёрки - машина якобы ездит по кругу с центром далёким от реальных координат.
- Реальное движение, неотличимое от шума - если у машина медленно движется по парковке, один и тот же эпизод может выглядеть и как "дрейф на стоянке", и как "медленное движение". Простой алгоритм путает одно с другим.
Если показывать всё это пользователю как есть - история поездок может превратиться в кашу. Все последующие аналитические выводы окажутся неточными, потому что строятся на загрязнённых данных.
Стандартное решение - простые пороги ("отбросить точки со скоростью > 200 км/ч") - не справляется. Спуфинг часто даёт правдоподобные скорости. РЭБ - тоже. Дребезг на парковке - это вообще не про скорость, это про дисперсию координат.
Подход и решение
В проекте реализован pipeline из 10 последовательных шагов, каждый из которых решает свою часть задачи. Шаги выполняются в строгом порядке, и между шагами передаются метаданные - например, шаг 4 может пометить точку как подозрительную, шаг 7 это учтёт при принятии решения. Это даёт совокупный эффект, который не достигается ни одним шагом по отдельности.
- Дедупликация. Точки с одинаковым timestamp'ом (бывает при дублировании пакетов из-за переподключения трекера) отбрасываются.
- Anti-spoof по одометру. Сравнение пройденного по GPS расстояния с показаниями одометра (если они приходят с CAN-шины). При расхождении выше порога - точка маркируется как подозрительная.
- Resolve coordinates. Точки с LBS-координатами (см. историю про каскадную геолокацию) обрабатываются и помечаются
coord_approximate. - LBS-маркировка спуфинга. Если расстояние между GPS и LBS координатами точки выше определённого порога - это подозрительно. Маркируется флагом.
- Filter outliers с rollback. Точки, физически невозможные относительно соседних, отбрасываются - но с откатом хвоста: если выброс был принят как валидный, и из-за этого следующие 2-3 точки тоже попали в трек, они отзываются обратно.
- Filter teleport return. Случай, когда произошёл “выброс” трека и последующее возвращение в общую канву. Такая точка-выброс удаляется (и метаданные шага 5 учитываются здесь же).
- OBD speed mismatch. Сравнение скорости из GPS с показаниями скорости с CAN-шины. Если они расходятся в разы - точка подозрительная.
- Median distance filter. Проверка медианного расстояния до соседних точек. Точка, отстоящая от медианы соседних на множитель (например, 5x) - отбрасывается.
- Movement without odometer. Самое тонкое место. Если по GPS машина проехала 5 километров, а одометр не сдвинулся ни на километр - это не реальное движение. Это либо РЭБ либо спуфинг с подвижной точкой. Сегменты таких точек выбрасываются. Но важная задача - не выбросить возвращения к реальным точкам если история началась с ложных - поэтому привязываться к одометру жёстко - нельзя.
- Parking drift с snap-to-known. Дребезг GPS на парковке. Если скорость близка к нулю и точки прыгают вокруг известной точки парковки - все они схлопываются в эту известную точку. Машина не "вибрирует" на карте, она стоит там, где стоит.
Каждый шаг в отдельности - относительно простая логика. Сила - в их последовательности и взаимодействии. На выходе - очищенный трек.

ML-воркер: индивидуальные модели на каждую машину
Отдельный Python-микросервис с plug-in архитектурой детекторов. Каждая машина получает свои собственные обученные модели, а добавление нового детектора аномалий сводится к написанию одного класса и регистрации в массиве. Сейчас активны два детектора, архитектура расширяется без перекомпиляции основного приложения.
Проблема
Современная машина с CAN-шиной выдаёт десятки параметров: обороты двигателя, температура охлаждающей жидкости, нагрузка, расход воздуха, напряжение бортовой сети, давление в системе, скорость, передача и так далее. Из этих данных можно делать гораздо больше, чем простые алерты такие как "перегрев". Можно ловить деградацию узлов до того, как они откажут - генератор начал хуже заряжать аккумулятор, привод буксует чаще обычного, расход стабильно вырос на 15% при том же характере поездок.
Но "норма" для каждой машины своя. У КамАЗа другие нормальные значения нагрузки, чем у Газели. У старой машины с пробегом 300 тысяч и у новой - разные базовые показатели. У машины, которая возит щебень, и у машины, которая ездит по городу с равномерной нагрузкой - разные паттерны телеметрии. Универсальные пороги "если расход >X, то аномалия" не работают.
Решение - индивидуальные модели на каждую машину, обученные на её собственных данных. Каждая машина "знает" свои нормальные паттерны, и аномалия определяется относительно её собственной нормы, а не абстрактного эталона.
Вторая часть проблемы - расширяемость. Виды деградации, которые можно детектировать, разные: потеря тяги, состояние генератора, расход топлива, поведение системы охлаждения, паттерны переключения передач. Каждое - своя задача со своими признаками. Если каждый новый детектор требует переписывать весь сервис - добавление одной фичи занимает недели. Нужна архитектура, в которой детектор - это plug-in, который пишется отдельным модулем и регистрируется одной строкой.
Подход и решение
ML-функционал вынесен в отдельный микросервис на Python 3 - он работает рядом с основным приложением, но независимо. Это не каприз архитектуры, а практическое решение: ML-задачи имеют другие требования к окружению (pandas, scikit-learn, numpy - всё это Python-экосистема), другой режим работы (периодический, не реактивный), и держать их в основном NestJS-приложении было бы плохо для всех - для основного приложения, для ML-кода, для развёртывания.
Plug-in архитектура детекторов. Каждый детектор - это класс с двумя обязательными методами: train(machineryId, historicalData) - обучает модель на исторических данных конкретной машины и сохраняет её, и detect(machineryId, recentData) - применяет модель к свежим данным и возвращает оценку отклонений плюс пояснение.
Чтобы добавить новый детектор - пишется новый класс по интерфейсу, добавляется в массив, перезапускается воркер. Никаких изменений в основном приложении, в БД, в API. Сейчас в массиве два класса: TractionLossDetector (потеря тяги - сравнение текущей нагрузки на двигатель с типичной для машины при тех же условиях) и AlternatorHealthDetector (исправность генератора - паттерны изменения напряжения бортовой сети относительно оборотов).
Расписание. Воркер работает по двум регулярным процессам через apscheduler. Раз в неделю по воскресеньям: проходит по всем машинам всех компаний, для каждой машины и каждого активного детектора обучает модель на накопленных данных. Каждые 15 минут - detect_all: для каждой машины и каждого детектора применяет модель к свежей телеметрии за прошедший интервал. Если какой-то детектор выдал anomaly_score > 0.6 - генерируется алерт, который записывается в общую БД.
Изоляция нагрузки. apscheduler настроен с такими параметрами: нельзя одновременно запустить два процесса детекции - даже если предыдущий не успел завершиться, новый ждёт, если воркер был выключен и пропустил несколько интервалов запуска - будет выполнен только один раз, не вся очередь из “опоздавших”.
Хранение моделей. Каждая модель - это файл в Docker-volume, привязанный к machineryId и имени детектора. Volume переживает перезапуски и пересборки контейнера. Если модель отсутствует или устарела - train_all создаёт её заново на следующем еженедельном цикле.
Связь с основным приложением - только через БД. Воркер читает свежую телеметрию из общей PostgreSQL и пишет результаты (статусы моделей, обнаруженные аномалии) туда же. Никаких HTTP-эндпоинтов между воркером и основным приложением. Это даёт устойчивость: воркер можно остановить, пересобрать, переписать целиком - основная функциональность системы при этом не страдает.
Онлайн Карта под real-time нагрузкой: оптимизация маркеров
Императивная Leaflet-карта с десятками машин, каждая со своим состоянием и анимацией, обновляющаяся в реальном времени по WebSocket - без подвисаний, моргания и утечек памяти.
Проблема
Онлайн карта - главный экран системы. На ней “живут” все машины компании в реальном времени: едут, стоят, стоят на холостом ходу, превышают скорость, отображают наличие алертов, теряют связь. Каждое изменение должно отражаться на карте сразу - иначе диспетчер видит устаревшую картину, и вся ценность мониторинга в моменте теряется.
Технически у такой карты несколько одновременных требований, которые друг другу противоречат.
Маркер должен визуально кодировать состояние машины - не просто точка, а живая иконка: цвет показывает режим (едет / стоит на холостых / припаркована / оффлайн), форма показывает движется ли машина и в какую сторону, бейдж показывает критические тревоги (перегрев, низкий заряд, Check Engine), волны вокруг - что координаты приблизительные, госномер под маркером - для опознания. Это много визуальной информации.
Машин много, события идут постоянно. На парке в 30-50 машин WebSocket выдаёт обновления десятки раз в секунду - каждое движение, изменение данных телеметрии. Если на каждом обновлении пересоздавать SVG-маркеры - DOM захлебнётся, карта начнёт моргать, браузер съест всю память.
Стандартное решение через React-обёртки (react-leaflet с <Marker> компонентами) для такой задачи не подходит - на каждом ре-рендере он пересоздаёт DOM-узлы маркеров, что и приводит к проблемам с производительностью. Нужна работа напрямую с императивным Leaflet-API, но без привычных в React паттернов "сбросить и пересоздать".
Подход и решение
Было принято решение отказаться от обёрток react-leaflet и работать с Leaflet напрямую через useRef и эффекты, синхронизируя состояние карты с Redux-стейтом. Это позволяет точечно управлять жизненным циклом каждого маркера- добавлять, обновлять, удалять без массовых пересозданий. Поверх этого работает несколько техник.
Fingerprint-сравнение для отсечки лишних обновлений. Для каждой машины на каждом ре-рендере собирается короткая строка-отпечаток - конкатенация всех визуально значимых полей (скорость, обороты, направление, напряжение, температура, ошибки, статус, госномер). Если отпечаток не изменился - иконка маркера не пересоздаётся, дёргаются только координаты (это дёшево). Сравнение строк гораздо быстрее, чем создание нового SVG и присвоение его маркеру через DOM.
Кеш SVG-иконок для статичных состояний. Для маркеров режимов "стоит на холостых", "припаркована", "оффлайн" комбинации (режим × тревога × госномер × в фокусе) образуют конечный набор - и каждая такая комбинация повторяется между разными машинами и моментами времени. Эти иконки кешируются по составному ключу и переиспользуются. Маркеры в режиме "движется" в кеш не идут - у них уникальный direction (угол поворота стрелки), и попадания в кеш были бы единичными.
Глобальные стили в <head> один раз. Анимации маркеров (пульсирующие столбики у режима "холостые обороты", расходящиеся волны у приблизительных координат, мигающий красный бейдж у критической тревоги) реализованы через CSS keyframes. Они добавляются в <head> страницы один раз при первом рендере карты - не дублируются внутри каждого SVG-маркера. На парке в 50 машин это разница между 50 копиями <style> в DOM и одной.
Diff-based синхронизация маркеров. Когда из стора приходит новый список машин, не происходит полная перерисовка. Вместо этого: существующие маркеры обновляются (если изменились по fingerprint), новые добавляются, исчезнувшие удаляются - индивидуально, по id. Это даёт устойчивую работу карты при потоке real-time событий без моргания.
Стабилизация контейнера перед fitBounds. Отдельная проблема: при первом рендере карты вызвать fitBounds() нельзя, пока контейнер не получил окончательную высоту - иначе карта отцентрирует машины неправильно, и при следующем layout-проходе они "уедут" за границы экрана. Решение: после монтирования карты ждать через requestAnimationFrame, пока высота контейнера не стабилизируется на трёх кадрах подряд, и только тогда вызывать invalidateSize() + fitBounds(). Это устойчивое решение классической проблемы моргания карты при загрузке.
Module-level canvas для измерения текста. Для определения ширины подложки под госномер нужен canvas.measureText. Чтобы не создавать canvas на каждом маркере, он вынесен в module-scope и переиспользуется. Мелочь, но на парке в сотни машин эта мелочь экономит сотни DOM-аллокаций.
Все эти техники работают в комбинации. По отдельности каждая даёт небольшой прирост, вместе - дают плавную работу карты на парке любого реального размера, без моргания, утечек памяти и тормозов при WebSocket-обновлениях.
Дополнительные технические решения
Помимо шести главных разделов выше, в системе работает ряд более коротких, но содержательных решений. Каждое из них самостоятельно - это не "вспомогательный код", а полноценные модули с собственной логикой.

Реактивный мониторинг и алерты
Параллельно с ML-аналитикой работает контур реактивного мониторинга - детекция критических состояний машины в моменте по простым правилам, без обучения моделей. Восемь типов алертов: перегрев двигателя, низкое напряжение бортовой сети, обрыв питания, длительная работа на холостых, превышение скорости, ошибка двигателя (Check Engine), отсутствие связи с трекером, низкий уровень топлива.
Каждый алерт реализован как стейт-машина с подтверждением, чтобы избежать срабатываний на короткие артефакты сигнала. Например, CheckEngineDetector срабатывает не на одном пакете с флагом ошибки, а только когда флаг присутствует 3 точки подряд - короткий сбой передачи или единичный шум фильтруется. LowVoltageDetector имеет отдельную логику игнорирования первых 20 секунд после запуска двигателя - в момент когда работает стартёр - напряжение проседает, и без исключения он бы регулярно выдавал ложные срабатывания.
Все детекторы используют cooldown с прогревом из БД (warmupCache): при перезапуске приложения в кеш загружается состояние последних срабатываний за последние сутки, чтобы не дублировать алерты, уже отправленные пользователю. Защита от race conditions - синхронный захват флагов до await, чтобы два одновременных пакета телеметрии не запустили одновременно две копии одного алерта.

Биллинг с гарантиями денежных операций
Система биллинга - учёт балансов компаний и списания по тарифам - построена с нуля как полноценный денежный контур.
Idempotency через idempotency_key - любая операция списания или пополнения требует уникальный ключ, и повторное обращение с тем же ключом не приводит к двойному списанию. Это критично, потому что cron-задачи списания могут запуститься в перекрывающихся окнах при сбое или перезапуске, и без идемпотентности компания теряла бы деньги по два раза за день.
Pessimistic_write блокировки - при изменении баланса транзакция захватывает строку компании в режиме SELECT ... FOR UPDATE, исключая параллельные изменения. Это защищает от классической race condition, когда два одновременных списания читают один и тот же баланс и оба пишут на основе устаревшего значения.
BigInt-арифметика на уровне приложения и БД - все суммы хранятся и считаются как целочисленные копейки, не как float. Это исключает накопление ошибок округления (классическая проблема "0.1 + 0.2 = 0.30000000000000004"), которые на длительной горизонтали приводят к расхождениям балансов.
Ledger-модель учёта - каждое изменение баланса записывается отдельной строкой в журнал транзакций с указанием суммы, типа операции, ссылки на исходную сущность (например, на машину, за которую списывается тариф), идемпотентного ключа. Текущий баланс компании - это не "поле в таблице", это сумма ledger'а. Это даёт полную аудируемость: для любого момента времени можно восстановить, как этот баланс получился.
Адаптеры протоколов трекеров
Современные GPS-трекеры используют разные бинарные протоколы поверх TCP: Sinotrack, Galileosky, Vega/Wialon-IPS - у каждого свои поля, свои форматы кадров, свои особенности (контрольные суммы, экранирование, режимы передачи).
В системе реализованы три адаптера, каждый на своём TCP-порту. Адаптеры решают одну задачу - приведение разнородных пакетов к унифицированному внутреннему формату (TrackerDataDto), который дальше обрабатывается всем стеком одинаково. Логика обработки телеметрии, треков, событий - не знает, с какого протокола пришли данные. Это даёт простоту развития: чтобы добавить поддержку нового протокола, достаточно написать новый адаптер с теми же выходными методами, никакие другие модули не меняются.
Адаптеры разворачиваются в составе основного приложения, но Traefik разводит TCP-подключения по портам (9090, 9091) - на каждом порту слушает свой парсер. Это позволяет одной машине одновременно обслуживать трекеры разных вендоров без пересборки и переконфигурации.
Real-time через WebSocket
Live-обновления карты, телеметрии и событий построены на Socket.io. Сервер использует @nestjs/websockets с @nestjs/platform-socket.io, аутентификация - через токен в handshake.auth при подключении.
Ключевое архитектурное решение - изоляция событий по компаниям. Каждый клиент при подключении ассоциируется со своей компанией через client.data.companyId, и broadcast'ы отправляются только клиентам из той же компании. Это значит: пользователь компании А никогда не увидит данные о машине компании Б, даже если бы захотел "подсмотреть". Изоляция работает на уровне кода диспетчеризации, не на уровне фронта.
Real-time каналы организованы через комнаты: map (актуальные позиции машин), отдельные комнаты на детальные экраны (telemetry конкретной машины, история треков). Клиент подписывается на нужные комнаты, отписывается при уходе со страницы.
Мультитенантность и общесистемные справочники
Платформа спроектирована как мультитенантная - на одной инсталляции могут работать несколько компаний, и их данные строго изолированы друг от друга. Каждая операционная сущность (машины, трекеры, точки треков, телеметрия, поездки, события, документы, ремонты, биллинг) содержит поле company_id, и все запросы к этим данным фильтруются по нему на уровне сервисов.
Параллельно с этим в системе есть слой общесистемных справочников - данные, расшаренные между всеми компаниями: коды ошибок двигателей с поиском, библиотека технической литературы (мануалы, схемы, инструкции). Любая компания может пользоваться ими, никакая не может их редактировать "под себя".
Связь между справочниками и операционными данными работает автоматически: для конкретной машины подбирается релевантная литература по марке и модели, а CAN-коды ошибок расшифровываются из справочника прямо в карточке проблемы или алерта. Это снимает с механика ручную работу по подбору документации.
Операционные данные изолированы по компаниям, эталонные знания общие. Многие системы делают эту границу размытой - либо дублируют справочники по компаниям (избыточность данных, расхождения между копиями), либо случайно "сливают" приватные данные через общие таблицы. Здесь граница чёткая и закреплена на уровне схемы БД.
Селективное сохранение телеметрии
Поток телеметрии с трекеров - это очень много данных. Машина в движении передаёт пакет каждые 10-20 секунд, парк из 30 машин - это несколько пакетов в секунду. Если сохранять всё - БД будет расти на сотни тысяч записей в сутки, и большая часть этих данных не несёт никакой полезной нагрузки (машина едет с постоянной скоростью по прямой или вообще стоит).
TelemetryFilterService решает задачу селективного сохранения: каждый пришедший пакет проходит через многокритериальный фильтр и сохраняется только при наличии веских причин. Критерии включают: переходы зажигания (включилось/выключилось), переходы двигателя (завёлся/заглушился), появление или исчезновение Check Engine, попадание в критические состояния (перегрев, низкое напряжение), значимое изменение координат относительно последней сохранённой точки, истечение адаптивного heartbeat'а (для движущейся машины - раз в минуту, для стоящей - раз в 15 минут).
Отдельный контур обработки - для архивных точек: иногда трекер задерживает передачу и присылает пакеты пачкой задним числом. Эти пакеты попадают на отдельный путь обработки, чтобы не сбивать стейт-машины реактивных детекторов (которые работают на актуальном "сейчас", а не на восстановлении прошлого).
Селективное сохранение даёт примерно 5-10-кратное снижение объёма данных в БД при сохранении полной картины всех значимых событий.
Downsampling графиков с сохранением экстремумов
На графиках телеметрии (топливо, напряжение, обороты двигателя, скорость) отображаются временные ряды за выбранный период - день, неделю, месяц. Если за месяц у машины накопилось 300 тысяч точек, попытка нарисовать их на графике с шириной 800 пикселей бессмысленна и тяжела для браузера: точки сольются в сплошную линию, а рендеринг займёт секунды.
Решение - downsampling на стороне бэкенда перед отдачей данных на фронт. Период разбивается на N интервалов (типично 200-500), и для каждого интервала возвращается набор репрезентативных точек: первая и последняя точки интервала, минимум и максимум за интервал. Это сохраняет визуальную форму графика — все экстремумы (пики и провалы), все переходы и плавные тренды видны, как если бы рисовалась полная кривая.
Без downsampling'а при простом усреднении, график "потеряет" короткие резкие события (минутный скачок напряжения, секундный провал давления). С простым LIMIT N без выбора экстремумов - не отобразятся пики между сэмплами. Текущий подход даёт визуально точную картинку при объёме данных, который браузер легко рендерит.
Инкрементальные дневные агрегаты телеметрии
Многие отчёты по парку строятся на дневных агрегатах: суточный пробег, суточный расход топлива, суммарное время движения за день, количество событий за день. Каждый раз пересчитывать эти агрегаты по всему массиву телеметрии - медленно и расточительно: за месяц по машине набирается 30 одинаковых вычислений на одних и тех же данных.
Реализован контур инкрементального обновления дневных агрегатов. По мере поступления новых точек телеметрии в БД отдельный механизм обновляет соответствующие записи в таблице daily_aggregates (по машине, по дате, по типу метрики). Старые дни закрыты и не пересчитываются. Текущий день - обновляется при поступлении новых данных.
Отдельная сложность - архивные точки, которые приходят задним числом (трекер задержал передачу из-за потери связи). Для таких случаев механизм агрегирования умеет открывать "закрытый" день для пересчёта, добавлять в него точку, обновлять агрегат и снова его закрывать. Это исключает накопление неточностей в исторических отчётах.
Получаемая из этого выгода - отчёты по парку отрисовываются мгновенно: запросы идут не к десяткам тысяч точек, а к десяткам строк в daily_aggregates.
Модуль ТО, ремонтов и проблем
Помимо мониторинга в моменте, система ведёт полную историю обслуживания техники. Это отдельная подсистема со своей моделью данных, связанная с машинами, но логически независимая.
Регулярное ТО - для каждой машины настраиваются интервалы технического обслуживания по трём осям: пробегу (например, "замена масла каждые 10 тыс. км"), наработки и времени ("замена фильтра раз в 6 месяцев"). Система отслеживает текущий пробег и дату последнего обслуживания, рассчитывает остаток до следующего ТО и подсвечивает приближающиеся сроки в таблице.
Журнал проблем - записи о неисправностях с описанием, фото, моточасами, пробегом, типом и приоритетом. К каждой проблеме привязываются задачи (диагностика, ремонт), история комментариев, документы.
Журнал ремонтов - что было сделано, когда, кем, какие запчасти потрачены. Фиксация фактически выполненных работ по проблеме.
Журнал диагностики - задача по диагностики, может быть создана на основе зафиксированной проблемы и будет хранить в себе статус и результат, если она завершена.
Все контуры (ТО, проблемы, ремонты, диагностика) объединены в общую модель истории обслуживания машины - открывая карточку конкретной машины, владелец парка или механик видит всё в хронологическом порядке: что делали, когда сломалось, что заменили, когда следующее ТО.
ИИ-анализ проблем техники через структурированные запросы
Когда механик заносит в журнал новую проблему по машине (с описанием, фото, моточасами, пробегом) - система отправляет запрос в Perplexity API для получения структурированного анализа. В запросе передаются параметры техники (марка, модель, год, тип двигателя, тяги, трансмиссии) и описание проблемы; в ответ возвращается JSON с гипотезами причин (с оценкой вероятности), пошаговой диагностикой и шагами ремонта с инструментами и запчастями. Результат сохраняется в карточке проблемы.
Ключевая деталь - строгая JSON-схема в запросе через параметр response_format: { type: 'json_schema', json_schema: ... }. Модель не возвращает свободный текст, который потом приходится парсить регулярками; она обязана выдать JSON ровно по заданной схеме, с гарантированными типами полей и обязательностью ключей. Это современный подход к работе с AI API, который снимает целый класс багов .
К одной проблеме может быть привязано несколько задач - диагностики и ремонтов. В карточке проблемы видна вся история "от обнаружения до устранения". Сейчас модуль временно выключен, чтобы не отвлекать от работы над основной частью системы; будет включён в следующем этапе развития проекта.
Самописный прокси-сервер карт с кешированием
Карты в системе - на базе Leaflet с тайлами от открытых источников (OpenStreetMap, OpenTopoMap). Прямое обращение фронта к этим источникам имеет два недостатка: трафик уходит на сторонние домены (медленно, ограничения по rate-limit от OSM, риск блокировок), а с учётом масштаба парка - каждое открытие карты пользователем тянет десятки тайлов. Чтобы решить эту проблему, в стек добавлен отдельный микросервис tile-proxy, написанный на Node.js.
Он перехватывает запросы вида /{z}/{x}/{y}.png, проверяет локальный кеш на диске (TTL 6 месяцев, общий лимит 10 ГБ), при попадании отдаёт сразу. При промахе обращается во внешний источник с two-tier ротацией: сначала между несколькими хостами одного источника (a/b/c.tile.openstreetmap.org), при множественных ошибках 429/403 — переключение на следующий источник (OpenTopoMap). Полученный PNG-тайл конвертируется в WebP (экономия трафика на отдачу - 30-40%) и кешируется в обоих форматах. Раз в сутки запускается очистка кеша: сначала по TTL, потом, если всё ещё больше 10 ГБ - удаление самых старых до 9 ГБ.
Сервис разворачивается через Traefik по пути /tiles с TLS - пользователь видит карту со своего домена, а не от OSM. Это и приватнее, и быстрее, и не зависит от доступности внешних источников для просмотра уже посещённых районов карты.
Frontend-архитектура
Фронтенд приложения построен на React 19 + TypeScript + Redux Toolkit 2 + MUI 7, организован по принципу Feature-Sliced Design: код разделён по бизнес-фичам (машины, треки, телеметрия, биллинг, проблемы, ТО, алерты), каждая фича - самодостаточный модуль со своими slice, селекторами, типами и UI-компонентами. Это даёт ту же возможность, что и модули NestJS на бэкенде: добавлять новые фичи без переплетения с существующими. Всего около 20 страниц + общие компоненты, около 25 Redux-слайсов.
Помимо описанной выше шестой главной истории про оптимизацию маркеров на карте, во фронтовой части системы есть несколько решений, заслуживающих отдельного упоминания.
Корректная и отзывчивая работа фильтров: AbortController + debounce + markStale
Страница телеметрии - типовой пример сложной формы фильтрации: пользователь выбирает машину, период (от-до), и по этим параметрам подгружается дашборд с десятком графиков и сводок. На реальных датасетах запрос за месяц может тянуться несколько секунд. Если пользователь меняет даты - летит несколько запросов подряд, и без специальной обработки возникают сразу три проблемы: лишний трафик к серверу, "пустые" UI-моменты пока новый запрос летит, и race condition (поздний ответ дешёвого запроса перетирает свежий тяжёлый, на экране оказываются неверные данные).
Решение работает на трёх уровнях. Debounce на 400 мс задерживает запуск загрузки - пока пользователь крутит, фронт молчит, ждёт окончания изменений. AbortController через .abort() на промисе RTK-thunk'а отменяет предыдущий запрос на уровне сети - не "просто игнорируем ответ", а реально прерываем fetch. И отдельно - синхронный markStale() сразу при изменении любого фильтра помечает текущие данные в сторе как устаревшие, и UI-карточки моментально становятся тусклыми. Пользователь видит, что данные обновляются, и не сидит в недоумении пока летит запрос.
Это решает сразу три проблемы - производительность, корректность, UX - без подключения внешних библиотек, на штатных средствах RTK и React.
Code splitting: каждая страница - отдельный чанк, Leaflet - изолирован
Все страницы приложения подгружаются через React.lazy. Компонент карты дополнительно изолирован в свой чанк - Leaflet - тяжёлая библиотека, и её не имеет смысла загружать пользователю, который пришёл смотреть отчёты по ТО или биллинг. Это даёт два эффекта: первоначальная загрузка приложения быстрее (минимальный bundle на старте), и пользователи, не пользующиеся картой, никогда её и не подгружают.
Мобильная адаптация
Интерфейс работает не только в десктопе. Карта парка, карточки машин с актуальной телеметрией, выдвижная нижняя панель с деталями - всё адаптировано под мобильный экран. Это критично для целевой аудитории: диспетчер, механик или владелец компании часто находятся не за рабочим столом, а на месте - в гараже, на стройплощадке, в поле. Возможность открыть приложение со смартфона и увидеть текущее состояние парка в моменте - необходимость, а не опция.

Прочие штрихи
Несколько мелких, но важных решений работают в фоне на качество приложения. Кастомные stateSanitizer и actionSanitizer для Redux DevTools - тяжёлые массивы (история точек, история телеметрии) скрываются в инспекторе как [N points — hidden], иначе DevTools становится неюзабельным на реальных данных. React.memo для Tooltip'ов в Recharts - чарты пересоздают tooltip-компонент при каждом ховере, мемоизация снимает заметную часть нагрузки. Условный рендеринг серий и осей графиков - если у машины нет CAN-данных, оси температуры/нагрузки на графике двигателя не отображаются вообще, и это работает чисто без фолбэков. WebSocket-хук с явной обработкой переподключения - Socket.io по умолчанию не переподключается при разрыве со стороны сервера, и без ручного connect() карта молча теряет live-апдейты. URL-синхронизация фильтров на странице телеметрии - выбранная машина и период живут в search params, ссылку на конкретный отчёт можно скопировать, обновить страницу или поделиться.
Дорожная карта
Система находится в активной разработке. Ниже - направления развития, разделённые по горизонту планирования.
Ближайшие шаги
Включение модуля журнала проблем с ИИ-анализом. Модуль сейчас отключён, чтобы не отвлекать от работы над ядром. Включение - возврат к работающей интеграции с Perplexity API.
Подключение модулей складского учёта запчастей. В системе реализованы (но пока не активированы) модули заявок на запчасти, счетов, отслеживания связи "запчасть - счёт - ремонт". Их включение замкнёт цикл "неисправность→ диагностика → заявка на запчасти → счёт → ремонт" в едином документообороте.
Среднесрочное развитие
Расширение и доводка ML-детекторов. Архитектура ML-воркера готова. На очереди - анализ стиля вождения и детектор аномального расхода топлива (отклонение от индивидуальной "нормы" машины на типовых режимах). Параллельно - отладка уже работающих детекторов на реальных кейсах: накопление подтверждённых случаев и обратной связи по ложным срабатываниям детектора, уточнение порогов, доработка моделей.
Расшифровка и анализ кодов ошибок двигателя. Сейчас из CAN-шины извлекается только факт наличия ошибки (лампа Check Engine), и сейчас она работает как простой триггер для алерта. Следующий шаг - извлечение конкретного DTC-кода из протокола, его расшифровка по справочнику кодов ошибок (он уже есть в системе как общесистемный справочник), и совместный анализ с данными телеметрии на момент возникновения ошибки. Это превратит реактивный алерт "загорелась лампа" в диагностическое событие "ошибка такая-то, в момент её возникновения двигатель работал в режиме X, температура была Y, обороты Z - типичная причина такая, проверить вот это".
Передача фото и видео с регистраторов для разрешения спорных моментов. Часть современных трекеров умеет передавать снимки и короткие видеофрагменты - для разрешения спорных ситуаций (ДТП, претензии клиентов, разногласия с водителем). Расширение системы под этот канал данных, хранение медиа-материалов с привязкой к точкам маршрута и событиям.
Интеграция с 1С и сторонними CRM. Архитектура мультитенантности и API готовы к подключению внешних систем - для проектов с автопарком в производственной структуре это типовой запрос.
Мобильное приложение для слесарей. Сотрудник видит назначенные задачи (ТО, ремонты, диагностики), отмечает выполнение работы и прикладывает фото результата.
Долгосрочное направление
Платформа развивается как расширяемая система управления автопарком с упором на предотвращение поломок. Главный вектор - углубление диагностического слоя: совместный анализ телеметрии и кодов ошибок, индивидуальные ML-модели на каждую машину, постепенный переход от реактивного контроля ("сломалось - узнали") к проактивному ("идёт деградация - предупредили"). Это альтернатива готовым закрытым системам типа Wialon, в которых нет места собственным детекторам и адаптации под индивидуальные процессы клиента.