Датчик топлива врет: как отличить заправку от шума

Как SABTRANS читает датчик уровня топлива - где заправка, где шум, где слив, как сверять чеки топливных карт и почему наивные пороги врут.

16 мин чтениямониторингтопливодатчикиданные

Владелец парка спецтехники задает мониторингу два простых вопроса: сколько залили и сколько сожгли. На баках стоят датчики уровня топлива (ДУТ), трекер шлет их показания, платформа мониторинга рисует график. Кажется, что ответ уже есть.

Потом открываешь один грузовик за месяц. Платформа насчитала 184 заправки, а по топливной карте их было 8. Залито по датчику 2726 л, по карте 660. Расход в отчетах 74 л на 100 км, по чекам и пробегу выходит 19. Каждая из этих цифр выглядит точной до литра, и ни одной нельзя верить.

Расскажу, как в SABTRANS мы отличаем заправку от шума, ищем сливы и сверяем чеки топливных карт с тем, что увидел датчик.

Что на самом деле отдает датчик

ДУТ не знает литров. Он отдает сырой отсчет - число с АЦП. У исправных датчиков в нашем парке это примерно 3000-3700 единиц. Литры получаются из тарировочной таблицы: ее снимают при установке, проливая бак мерными порциями, и хранят в кабинете платформы мониторинга. У нас этой таблицы нет.

За прошедшие сутки платформа отдает калиброванные литры только в нескольких точках: уровень на начало и конец суток и уровень до и после каждой заправки или слива, которые она нашла сама. Все, что между ними, - сырой ряд отсчетов. Из него и приходится собирать картину.

Три свойства этого ряда определяют все остальное.

Ноль - это не пустой бак. Пропала связь, датчик сбросился - в ряду ноль. Если пустить его в расчет, каждый обрыв связи станет «сливом до дна». Поэтому сырой ноль у нас значит «нет данных». Цена решения: настоящий слив досуха может выпасть из ряда. Зато нет ложных обвинений.

Топливо плещется. В движении бак качает, на уклоне топливо уходит к одной стенке, после остановки оно успокаивается. Один и тот же бак на ходу и на стоянке читается по-разному, и разброс между этими состояниями доходит до 20-40 л. Это не поломка конкретного датчика, а физика жидкости в баке. С ней надо жить.

У шкалы есть потолок. 4095 - максимум двенадцатибитного отсчета. На том грузовике из вступления отсчет упирался в 4095: выше примерно 100 л датчик ничего не видел, и полный бак прочитать было нельзя. А в движении уровень прыгал на ±15 л каждые несколько минут. Платформа складывала эти зубцы: прыжки вверх становились заправками, падения - расходом. Отсюда 184 заправки и 74 л на сотню.

Никакой алгоритм это не чинит. Лечение механическое: демпфер, крепление, тарировка бака заново. Пока это не сделано, топливные отчеты по этой машине смотреть бесполезно, и честнее так и сказать, чем показать красивую цифру.

Сначала шкала

Раз таблицы нет, литры внутри суток приходится считать самим. Первая версия делала это пропорцией: брала уровень на конец суток от платформы, делила на сырой отсчет в тот же момент и получала «литров на единицу». Пропорция молча предполагает, что шкала проходит через ноль: ноль отсчетов - ноль литров.

Тарировочные таблицы через ноль не проходят. У одного тягача смещение шкалы около -90 л, у другой машины - сотни литров. С таким смещением пропорция дает неверный наклон, и любой прирост уровня выходит заниженным:

# Условный бак: шкала со смещением, как у таблицы, не проходящей через ноль
a, b = 0.26, -300.0          # литров на отсчет и смещение, л

raw = 3500                   # отсчет на конец суток
level = a * raw + b          # 610 л - этот уровень назвала платформа
k = level / raw              # пропорция «через ноль»: 0,174 л на отсчет

rise = 800                   # долив в отсчетах
print(a * rise)              # 208 л - залито на самом деле
print(round(k * rise))       # 139 л - по пропорции, готовый «недолив» в 69 л

Это не теория. У одной машины пропорция показывала около двух третей настоящего прироста (коэффициент 0,674): заправка по чеку на 240 л выглядела как 175 л и «недолив 65», хотя с правильным наклоном долив выходил около 260 л. За две недели на эту машину пришлось шесть из восьми верхних строк в списке недоливов - около 482 л из 713 по всему парку. В основном ложных.

Вторая ловушка - два бака. Цена деления у двух датчиков на одном тягаче отличалась примерно вдвое, а мы умножали сумму отсчетов на общий коэффициент. Заправляют обычно один бак, и его прирост занижался в 1,7 раза: при чеке на 400 л интерфейс показал +230 и «недолив 170 л». Все четыре недолива этой машины, 451 л, оказались ложными.

Теперь у каждой машины своя шкала: литры = c1 · ДУТ1 + c2 · ДУТ2 + смещение. Подбираем ее методом наименьших квадратов по опорным точкам. Опорная точка - момент, когда платформа назвала уровень в калиброванных литрах, а мы знаем сырые отсчеты в то же время. Сырое значение берем медианой по отсчетам в пределах десяти минут от момента: одиночный выброс ровно на границе суток иначе увел бы всю прямую.

За сутки таких точек от двух до шести, а неизвестных три. Поэтому точки копятся, и подгонка идет по окну в 120 суток. Дальше - защита от уверенной ерунды:

  • точки, ушедшие от прямой дальше двух с половиной средних невязок, выкидываются, и подгонка считается заново, но не больше двух раз;
  • шкала принимается, только если она физична: от 10 до 3000 мл на единицу отсчета, смещение не больше 500 л, средняя невязка не больше 25 л;
  • если второй бак за все окно почти не менялся, его коэффициент не определить, и машина считается однобаковой;
  • шкала, которую 45 суток не подтверждали свежие точки, протухает: датчик могли поменять.

На тягаче с двумя баками невязка своей шкалы вышла 3,2 л, а та самая заправка стала 396 л при чеке на 400.

Где своей шкалы еще нет, остается пропорция. Тогда у литров в интерфейсе стоит «≈», и человек видит, что число приблизительное.

Почему шкала идет первой, станет понятно ниже: все пороги в этой системе - в литрах. Если шкала занижает прирост на треть, порог в 60 л на деле стоит около 90. Подбирать пороги на такой шкале - значит подгонять их под ошибку.

Стоянка: вход, окно, осадка

Заправка и слив случаются на стоянке. Стоянки дает платформа, и сначала их пришлось собрать обратно: одну парковку она может разбить на три куска встык - 11:20-11:29, 11:29-11:30, 11:30-12:00. Мерить заправку по первому куску значит мерить посреди подъема уровня. Поэтому стоянки, между которыми не было настоящей поездки, склеиваются. Маневр по площадке АЗС - до двух минут и 300 м - поездкой не считается. А разрыв без единой записи о движении склеиваем, только если он не длиннее минуты: это может быть дыра в телеметрии.

Дальше у каждой стоянки меряются три уровня.

Уровень входа - медиана свежих отсчетов перед стоянкой: не старше трех минут, минимум две точки. Он нужен для быстрой заправки, когда уровень скакнул за первую минуту стоянки. Медиана начала окна такую ступеньку «съедает», и долив в 200 л показывался бы как 20.

Но уровень входа - это уровень на ходу, а между «на ходу» и «на стоянке» те самые 20-40 л. Поэтому вход берется базой, только если через границу стоянки уровень шагнул вверх не меньше чем на 60 л. Иначе база - медиана начала окна, уже на стоянке.

Окно стоянки - медианы начала и конца по непересекающимся половинам, до пяти точек на край. Меньше четырех точек в окне - по стоянке не судим: на двух-трех отсчетах один выброс дает ложную заправку.

Осадка после стоянки. На АЗС уровень часто «догоняет» уже после конца GPS-стоянки, пока машина выезжает. Поэтому смотрим еще полчаса после стоянки, но не дальше следующей. Хвост режем на непересекающиеся подокна, берем максимум их медиан и требуем, чтобы в полосе 15 л от него держались хотя бы два подокна: одиночный всплеск шума так не пройдет. Плато принимается, если оно на 60 л выше базы и на 15 л выше конца окна. Иначе это дрейф.

Ядро расчета в коде, все величины в литрах:

const REFUEL_MIN_L = 20; // рост на стоянке, с которого показываем заправку
const STEP_MIN_L   = 60; // шаг через границу стоянки: выше шума «на ходу / на стоянке»
const DRIFT_MIN_L  = 15; // плато над концом окна, чтобы не принять дрейф за долив

/**
 * Долив на стоянке или null, если заправки не было.
 * $enter   - медиана свежих отсчетов перед стоянкой (null, если их мало)
 * $start   - медиана начала окна, $end - медиана конца окна
 * $settled - подтвержденное плато после стоянки (null, если его нет)
 */
function refillLiters(?float $enter, float $start, float $end, ?float $settled): ?int
{
    $base = ($enter !== null && $start - $enter >= STEP_MIN_L) ? $enter : $start;

    $after = ($settled !== null
        && $settled - $base >= STEP_MIN_L
        && $settled - $end >= DRIFT_MIN_L) ? $settled : $end;

    // Честное нетто: ничего обратно не прибавляем.
    $rise = (int) round($after - $base);

    return $rise >= REFUEL_MIN_L ? $rise : null;
}

Здесь два разных порога роста, и это не небрежность. 20 л - когда сравниваются два уровня на стоянке: бак спокоен, медианы держатся плотно. 60 л - когда сравнение пересекает границу «ехал / стоял»: там шум 20-40 л, и порог обязан стоять выше него.

Слив считается зеркально, но с поправкой. Из падения уровня на стоянке вычитается легальный расход: спецтехника работает на месте, кран жжет топливо стоя. Платформа отдает расход за сутки целиком и отдельно в движении, разница - расход на месте. Каждая стоянка получает свою долю по времени, а остаток от 20 л становится строкой «возможно слив». Сливы меряются по исходным, не склеенным стоянкам: на склеенном окне поправка выросла бы вместе с ним и могла спрятать короткий слив.

Свой детектор у нас не главный. Если платформа сама нашла заправку, верим ей: у нее литры по тарировочной таблице. Наш детект добирает то, что платформа пропустила: мусорный отсчет посреди налива, замерший на двадцать минут датчик, налив, который сел уже в движении. Если наша заправка лежит в пределах получаса от платформенной, второй раз ее не показываем.

Долго казалось, что детекция заправок в кабинете платформы не включена: событий приходил ноль. На деле платформа присылала тип события строкой «FuelIn», а разбор ждал числовой код из документации и молча выбрасывал все. Таблица событий стояла пустой при исправном API. Теперь принимаем и код, и имя.

Нетто, а не «как было бы»

Самое поучительное решение в этой части - долив считается честным нетто: осевший уровень минус уровень входа. Ничего обратно не прибавляется.

Возьмем парковку, где топливо сначала слили, а потом долили. Слив уменьшает измеренный долив, и недолив по чеку выходит завышенным. Сам слив при этом виден отдельной строкой. Ошибка есть, но она работает в сторону показа недолива, а не его сокрытия.

Хотелось сделать «точнее» - вернуть потери на этой же парковке в объем долива. Было две попытки, обе откатили на ревью в сентябре.

Вернуть оценку расхода двигателя на месте. Выглядит симметрично: у сливов эта поправка вычитается, значит у доливов ее надо прибавить. На тестовых данных поправка на ровной стоянке целиком уезжала в «рост» и рождала заправку «300 → 321» на прямой линии. У настоящих заправок она завышала объем тем сильнее, чем длиннее стоянка: ночевка часов в десять добавляла 28 л, и недолив 30 л превращался в 2 л. Симметрии здесь нет намеренно. У слива ошибка оценки делает детект строже, это безопасно. У долива та же ошибка прячет недоливы, то есть работает в сторону денег.

Вернуть измеренный провал уровня. Это уже не оценка, а замер, но с порогом срабатывания 20 л. А порог лежал внутри шумового коридора датчика - тех самых 20-40 л, из-за которых пороги роста через границу стоянки стоят на 60. Минимальный недолив, который мы показываем, - 15 л, то есть меньше самого шума. Провал на 25 л, который на деле был просто шумом, проходил порог, прибавлялся к доливу и гасил настоящий недолив целиком. Вдобавок на границе возникала ступенька: провал в 19 л не добавляет ничего, а в 21 л - все 21.

Вернуть такую поправку можно, но при условиях. Порог - не ниже коридора шума, около 60 л. Прибавлять только превышение над порогом, а не всю величину, - тогда нет ступеньки. Перед включением - прогон по истории с числами: сколько заправок правило рождает на ровных стоянках и сколько настоящих недоливов гасит. И все это только после того, как шкала стала правильной: при неверном наклоне пороги в литрах стоят не там.

Сверка с топливной картой

Датчик отвечает на вопрос «сколько вошло в бак», карта - «сколько оплачено». Недолив - разница между ними. Звучит как вычитание, а на деле это самая хрупкая часть системы.

Сначала чек нужно привязать к машине. Транзакции карт забирает демон раз в четыре часа, окном от начала вчерашнего дня. Машину находим по госномеру из строки держателя карты. У одного поставщика держатель записан парой «ФИО;номер», но у полутора десятков карт там стояла одна метка «Модель: номер» без точки с запятой. Номер уходил в базу прочерком, и сверка по этим машинам не запускалась вовсе: по одному грузовику - ноль сверок при 27 чеках. Ни ошибки, ни предупреждения. Тот же урок, что в заметке про сторожа: смотреть надо на результат, а не на то, что процесс отработал.

Разбор починили, но демон тянет окно в сутки, и правка помогает только новым транзакциям. Старые строки останутся с прочерком, пока период не перезапросить руками - повторная загрузка находит дубль и обновляет номер. Иначе сверка за прошлые месяцы так и не начнется.

Время чека - это время проводки. Оно почти всегда позже физической заправки, иногда на пару часов, когда машина уже за полторы сотни километров от АЗС. Первая версия искала заправку по датчику, ближайшую по времени к чеку. В итоге чек на 250 л лип к случайной стоянке с шумом +20 в другом месте, и система показывала «недолив ≈230» при нормальной заправке. Теперь окно асимметричное: четыре часа назад от чека и час вперед. Главный критерий - место: названия из адреса АЗС против адреса стоянки, без родовых слов вроде «область» и без брендов сетей. Потом близость по времени. Пары назначаются взаимно однозначно: одна заправка не достается двум чекам.

Объем в выборе пары не участвует сознательно. Если выбирать заправку, ближайшую к оплаченному объему, выбор будет смещаться к самому маленькому недоливу, то есть прятать его.

Чеки одной заправки сводятся в группу: та же АЗС, в пределах часа от первого чека. Иначе заправка, разбитая на два чека, сверялась бы по частям.

Дальше формула: недолив = оплачено - вошло в бак - возврат АЗС. Возврат приходит обычно в пределах 18 минут после заправки, это компенсация недолива, а не отдельная операция. Недолив от 15 л получает статус «недолив». И несколько исключений, каждое из живого случая:

Статус Когда Почему
мало для сверки оплачено меньше 20 л мелкую дозаправку датчик не отличает от шума
датчик без данных своего ряда отсчетов за сутки нет судить об отсутствии налива не на чем
заправка через полночь чек ближе получаса к границе суток рост за эти сутки виден только частью
бак был полон уровень после заправки в 20 л от потолка бака последние литры физически не поместились
оплачено, а налива нет чек есть, роста уровня рядом нет это факт, а не вывод о вине

Про «бак был полон» стоит сказать отдельно. Чек на 200 л, датчик +148, платформа +151. Два источника согласны, значит недолив 52 л? Нет: платформа и наш ряд читают один и тот же датчик, их согласие - не независимое подтверждение. Бак дошел до горловины, и последние литры не поместились. Потолок бака берем из истории: максимум уровня после заправок, но только если машина подходила к нему хотя бы три раза и второй по величине уровень лежит рядом. Иначе одиночный высокий отсчет сам задал бы потолок и сам снял бы свой недолив.

Еще одна частая история - водитель заправил машину картой соседней. По номеру чек уезжает не туда: у настоящей машины «налива нет», у чужой ложный недолив. Для этого есть ручная привязка чека к технике. Она сильнее номера, и ночной пересчет ее не затирает.

Что видит человек

Диспетчер и владелец открывают карточку дня машины: график уровня в литрах (со значком «≈», если шкала приблизительная), сколько было в каждом баке утром и вечером, заправки в виде «было → стало», возможные сливы и чеки по карте с итогом сверки. На графике подсвечена та заправка, с которой сверился чек.

Флотовый отчет «Недоливы» собирается ночью: недоливы по парку, «оплачено, а налива нет» в литрах и рублях, возвраты АЗС и то, что не поместилось в полный бак. Формула написана прямо в шапке, чтобы любую цифру можно было проверить руками.

Категория «оплачено, а налива нет» оказалась самой денежной: именно в ней находятся литры, за которые заплатили, но которых нет в баке этой машины.

Рядом со строкой недолива есть блок «Куда могло уйти». Он не обвиняет, а показывает совпадения: чеки по другим машинам на той же АЗС в пределах двух часов и наливы по датчику у других машин парка в то же время. Самый частый ответ у клиентов - не слив, а «этой картой заправили другую технику». Вывод делает человек.

Отчет по расходу помечает аномальные сутки: расход на моточас вдвое выше обычного для этой машины и не меньше 15 л в час. Чего пока не хватает в бою - отсечки «датчик врет». На грузовике из вступления отчет по заказам показывал 74 л на сотню со статусом «Норма»: статус считался по пробегу, а расход платформы шел как факт. Следующий шаг уже написан в отдельной ветке, но еще не в бою: норма расхода на машину - по карте, по разнице уровней, справочная и ручная. Если датчик расходится с картой в полтора раза, вместо литров в отчете будет стоять «датчик».

Чему это научило

Порог не может быть внутри шума. Сначала меряешь шум, потом ставишь порог выше него. И помнишь, что шум зависит от того, что с чем сравниваешь: два уровня на стоянке и уровень на ходу против уровня на стоянке - разные задачи с разными порогами.

Сначала шкала, потом пороги. Порог в литрах имеет смысл, только если литры настоящие. Неверный наклон двигает все пороги сразу, и никакая их подстройка этого не исправит.

Ошибка должна быть в безопасную сторону. Где ошибка неизбежна, выбираешь ее направление. Долив - нетто, даже если иногда он занижен: лучше показать лишний недолив, который человек проверит, чем спрятать настоящий. Нет своего ряда - «датчик без данных», а не «налива нет». Одиночный высокий отсчет - не потолок бака.

Проверяй правило прогоном по истории. Любое правило, которое меняет денежные цифры, сначала прогоняется по накопленным дням: сколько заправок оно рождает на ровном месте и сколько недоливов гасит. Обе откаченные поправки выглядели разумно на одном примере и ломались на соседнем.

Плохой датчик кодом не чинится. Если отсчет упирается в потолок, а уровень прыгает на 15 л, нужен установщик, а не новая эвристика. Задача софта здесь - честно сказать, что данным этой машины верить нельзя.

Датчик топлива врет не со зла. Он честно меряет жидкость, которая плещется в баке на кочках. Врут выводы, сделанные из его цифр без своей шкалы, без порогов выше шума и без проверки на истории.

Esc

↑↓ выбор Enter открыть