Всем привет, на связи Фарук, инженер-разработчик электроники и встроенного ПО в Whoosh (читается как ВУШ, ощущается как вжууух). Работаю я в embedded отделе (хардкорные программисты, что пишут прошивку на C для различных железок и проектируют эти самые железки), но в основном занимаюсь анализом различных данных от нашего IoT модуля и разработкой алгоритмов для работы с этими данными.
Наша компания — сервис аренды электросамокатов (а местами еще и электровелосипедов) или, иными словами, кикшеринг. О том, как мы к этому пришли и что из себя представляем можно почитать здесь.
Одно из отличий использования шерингового самоката от личного — наличие определенных правил. Например, вы видели когда-нибудь парочку влюбленных, вдвоем на самокате, исчезающих в закате? Или может наблюдали троих парней, которые в обнимку, преодолев смущенье, едут навстречу новым приключеньям? А может быть вы видели как чей-то отец, словно швец, жнец и на самокате ездец, с одним ребенком подмышкой а с другим на шее смело едет по парковой аллее?
Вызывают ли у вас эти картины гнев и праведное негодование? А может быть вы и сами не прочь прокатиться с другом/подругой на одном самокате? У нас для вас есть две новости. Во-первых, так нельзя. А во-вторых, добро пожаловать под кат.
Почему же все-таки вдвоем нельзя?
Наш сервис запрещает поездки вдвоем по ряду причин: самокат не рассчитан на двух человек; вдвоем маневренность ухудшается, как и время реакции; тормозной путь у двоих человек ощутимо больше и так далее. Всё это делает такие поездки небезопасными как для окружающих, так и для самих пользователей. Во всех коммуникациях с пользователями мы продвигаем правило “1 человек=1 самокат” и рассказываем об этом на Школе Вождения. Однако всё же есть те, кто осознанно это правило нарушает и те, кто даже не подозревает о нем (да-да, все мы читаем оферту не глядя). В связи с этим перед нами встает задача правильного и точного определения таких ситуаций.
Есть идеи, как определять?
В голову сразу приходит CV (Computer Vision). То есть, если где-то на самокате установить камеру, которая будет видеть людей на нем, то, обрабатывая видеопоток с этой камеры, можно будет определять их количество. Ниже как раз представлено изображение с камеры, установленной на самокате. Правда тестировали мы тогда совсем другое и местоположение камеры в данном случае больше подходит для обзора того, что перед самокатом, а не того, что на самокате. Но это дает небольшое понимание возможностей CV.
Сходу возникает несколько вопросов относительно угла обзора и оптимального местоположения камеры, и как минимум одна ситуация, при которой от камеры не будет толку. А именно: когда один человек полностью заслоняет другого (других?) или, например, сидит у него на плечах. Ну и конечно же, установить на каждый самокат модуль CV — задача нелегкая & небыстрая.
Впрочем, идея с CV кажется очень даже неплохой, пусть и не для определения количества людей на самокате, а, например, для определения препятствий перед самокатом. И в одной из следующих статей мы расскажем об этом чуть подробней.
Но вернемся к нашей задаче. Раз уж мы решили не пытаться определять количество людей визуально, можно попробовать сделать это при помощи датчиков. Существуют резистивные сенсоры силы, основанные на карбоновом полимере, который изменяет своё электрическое сопротивление под внешним воздействием.
Например, панель, как на изображении слева, при помощи большого количества сенсоров, расположенных по всей своей площади, способна измерять не только общее давление на поверхность, но и то, как оно по этой поверхности распределено. Такие панели используются в медицине и при создании эргономичных матрасов, сидений и пр.
И нашу задачу они могли бы решить, если правильно обрабатывать данные с этих панелей, то есть определять стопы и их количество, фильтровать каблуки и так далее. Хотя конечно и в таком случае не исключены ситуации, когда полученные данные трудно интерпретировать.
Трудно интерпретируемые данные и их источник
Впрочем, как бы заманчиво это ни звучало, превращать деку самоката в одну цельную сенсорную панель мы не будем. Но вот установка тензодатчиков звучит как вариант. Например, есть такой отладочный комплект MAXREFDES82, в котором целый набор тензодатчиков, и обрабатывая их измерения, он выдаёт как суммарно приложенную силу, так и точку её приложения.
Вот видео с описанием того, как он работает. В указанном примере используется 4 датчика, и этого достаточно, чтобы определять одну точку приложения сил. Но кажется, если эту систему масштабировать и использовать больше датчиков, то соответственно можно определять больше точек приложения силы. Однако, установка подобных систем, особенно в деку самоката, приводит к существенному усложнению конструкции и, как и в случае с CV, получается нелегко & небыстро.
А какие датчики и данные мы уже имеем на самокате?
В первую очередь, мы можем получить от контроллера мотора скорость вращения колеса и ток с напряжением, подаваемые от батареи. Используя скорость колеса, а также информацию об её изменении во времени, мы можем получить линейное ускорение. Для этого реализуем простой фильтр Калмана. О нем на хабре есть немало статей, например вот эта, или эта, а ещё вот эта. Поэтому мы в детали вдаваться не будем, а просто покажем как примерно мы реализовали его у себя в прошивке IoT модуля на языке C.
/**
* SIGMAs are the Kalman filter parameters
* they define how inert the filter will be
* i.e. smoothness vs accuracy of given acceleration
*/
#define SIGMA_X 0.1f
#define SIGMA_Z 0.005f
/**
* @brief State Vector for Kalman Filter
* @param X0 wheel speed
* @param X1 acceleration
*/
typedef struct
{
u32 TS;
float X0;
float X1;
float P00;
float P01;
float P10;
float P11;
} LinearAcceleration_t;
static LinearAcceleration_t LineAcc = {
.TS = 0,
.X0 = 0,
.X1 = 0,
.P00 = 1,
.P01 = 0,
.P10 = 0,
.P11 = 1
};
for(;;)
{
RingList_ExtractCell(measurementsRingList, (void*)&NewMeasurement,
RingList_GetLatestRingIdx(measurementsRingList), NULL);
float dt = NewMeasurement.TS - LineAcc.TS;
/**
* Prediction step
* Computing vector state elements using previous Acceleration and dt
*/
float X0pred = LineAcc.X0 + LineAcc.X1 * dt;
float P00pred = LineAcc.P00 + (LineAcc.P01 + LineAcc.P10) * dt + LineAcc.P11 * dt * dt;
float P01pred = LineAcc.P01 + LineAcc.P11 * dt;
float P10pred = LineAcc.P10 + LineAcc.P11 * dt;
float P11pred = LineAcc.P11 + SIGMA_X * dt;
/**
* Kalman gain
*/
float K0 = P00pred / (P00pred + SIGMA_Z);
float K1 = P10pred / (P00pred + SIGMA_Z);
/**
* Update step
* Computing new state vector based on prediction and given measurements
* using Kalman gain
*/
LineAcc.X0 = X0pred + K0 * (NewMeasurement.Wheel - X0pred);
LineAcc.X1 += K1 * (NewMeasurement.Wheel - X0pred);
LineAcc.P00 = (1 - K0) * P00pred;
LineAcc.P01 = (1 - K0) * P01pred;
LineAcc.P10 = P10pred - K1 * P00pred;
LineAcc.P11 = P11pred - K1 * P01pred;
LineAcc.TS = NewMeasurement.TS;
}
В коде может показаться не совсем понятным, откуда на каждой итерации мы берём новые измерения. Для этого мы используем функцию RingList_ExtractCell, при помощи неё мы достаем последнее актуальное значение из кольцевого буфера, в который мы их складываем внутри другой задачи. Таким образом нам удается поддерживать работу с данными внутри прошивки, в которой параллельно работает множество задачек, каждая из которых может как доставать, так и добавлять новые данные в различные кольцевые буфферы, не нарушая атомарности данных и поддерживая их актуальность. Получается достаточно интересный подход, который мы с удовольствием опишем в одной из следующих статей.
Вообще, ускорение можно было бы считать как есть, по формуле Тогда и фильтр никакой не нужен. Однако в таком случае, значения получились бы очень зашумленными. График ниже как раз для сравнения показаний, отфильтрованных и сырых.
Также, перемножая напряжение батареи и потребляемый ток, мы можем получить потребляемую мощность Если учесть, что при движении IoT модуль, дашборд и подсветка потребляют пренебрежительно мало относительно мотор-колеса (десятки мА против десятков А), то можно сказать, что В итоге мы имеем мощность, скорость и ускорение, графики которых для одной из тестовых поездок представлены ниже.
Немного из курса школьной физики
Во время поездки на самокате мощность мотор-колеса расходуется на придание ускорения, преодоление силы трения и изменение потенциальной энергии.
В рамках данного исследования мы сознательно пренебрегаем сопротивлением воздуха и инерционными потерями во вращающихся деталях самоката. Первое несущественно при возможных скоростях самоката и при этом измерить его и компенсировать у нас всё равно нет возможности, а второе практически отсутствует, так как отсутствуют трансмиссия или маховик, обычно являющиеся причинами подобных потерь. Также мы пренебрегаем потерями в подшипниках мотор-колеса. При попытках осознать, что происходит с электрической энергией получаемой от батареи и какие присутствуют потери, мы ориентировались на [1] и пытались применить это к самокатам. Однако так просто оставлять столько неизвестных мы не намерены и уже озадачились созданием самокатного диностенда. По итогам расскажем, что у нас получилось.
Иными словами всё, происходящее с самокатом во время поездки, можно записать в виде формул, используя Второй закон Ньютона или Закон сохранения энергии:
или
где — масса самоката с пользователем, — угол уклона дороги, — путь пройденный за время , а остальные переменные вроде должны быть понятны.
На самом деле, вторая формула преобразуется в первую, если использовать: , , , и сократить всё на , а также для первого использовать .
Итого, для определения массы пользователя (вместе с самокатом) в конкретный момент времени можно использовать формулу:
Если взять формулу выше и упростить её, положив , и , то:
, а график полученный по этой формуле и данным выше, выглядит так:
Видно, что во время разгона (не считая первой секунды, когда пользователю нужно оттолкнуться ногой, чтобы поехать) и при поддержании равномерной скорости, вычисляемые значения массы колеблются в пределах одного значения — около 130кг, а это очень даже похоже на правду. Таким образом, когда расходуемая мощность, скорость и ускорение положительны, мы можем приблизительно определять массу пользователя с самокатом по формуле
Однако есть нюансы
Мощность мотор-колеса
Мощность тяги из формулы выше не есть то же самое, что и электрическая мощность мотор-колеса. Во-первых, она механическая и зависит от тока мотора и его характеристик. Во-вторых, нам необходимо знать КПД . В-третьих, что ты мне сделаешь, я в другом городе, за КПД извени нам не известен коэффициент сцепления с дорогой, который к тому же полностью зависит от окружающих условий. Но кажется, что в первом приближении и за неимением всех необходимых данных, нам остается лишь положить .
Неизвестные коэффициенты
Для того, чтобы получить приемлемую точность определения массы, нужно определить следующие коэффициенты: и . Причем они также могут зависеть от внешних факторов (напр. покрытие дороги) и меняться в зависимости от входных данных (напр. зависимость от мощности или скорости). Учитывая, что идеально точно определить эти коэффициенты у нас всё равно нет возможности, можно использовать усредненные, фиксированные и экспериментально определенные значения, поэтому с и мы так и поступили.
Также для определения массы нам нужно знать угол уклона дороги. Однако, на практике оказывается, что это не так уж и просто.
Угол уклона дороги
Вернемся к тому, что мы уже имеем на самокате. Со скоростями и ускорениями вроде стало понятно, а вот что у нас по датчикам? В наш IoT-модуль, помимо всего прочего, установлены акселерометр, гироскоп, магнитометр и барометр. Первые три обычно используются для определения ориентации в пространстве (вместе их иногда называют MARG и применяются они в AHRS). Если добавить к ним барометр, то можно еще и определять высоту, как обычно делают в квадрокоптерах. Но в самокатах-то высота зачем, они же (пока еще) не летают?
Напомним, для определения массы по формуле выше, нам нужно знать угол уклона дороги, и с ним есть небольшие трудности:
-
необходима достаточно высокая точность определения угла
-
ориентация в пространстве, определяемая при помощи MARG не совсем подходит, так как она показывает угол IoT модуля, а не самоката в целом (т.е существует смещение, которое мы можем определить только примерно)
-
угол полученный от MARG к тому же подвержен влиянию линейного ускорения, которое к слову можно скомпенсировать, но это в свою очередь приведет с дополнительному снижению точности.
И тут-то нам на помощь приходит барометр! Используя формулу , где , можно получить .
Для того, чтобы убедиться, что барометр все-таки подходит для измерения высоты и, как следствие, угла уклона дороги, мы совершили тестовую поездку и записали в ней показания барометра. Также, для каждой координаты этой поездки при помощи сервиса https://open-elevation.com/ была определена высота относительно уровня моря. А при помощи сервиса https://open-meteo.com/ было определено атмосферное давление и температура воздуха в городе на момент старта тестовой поездки.
Далее, по показаниям барометра при помощи барометрической формулы с использованием полученных температуры и давления, были рассчитаны высоты для каждой точки трека.
На графике ниже представлены значения высот, полученные с использованием показаний барометра и по GPS координатам.
Видно, что отличаются они несущественно и, что немаловажно, формы графиков совпадают. Причем кажется, что определенная по барометру высота даже точнее. Таким образом получается, что по барометру можно отслеживать относительное изменение высоты и, как следствие, примерное значение угла уклона дороги.
Получается, если собирать данные со всех самокатов, фильтровать и агрегировать, то можно построить карту высот всех дорог, по которым эти самокаты ездили. Звучит заманчиво, и мы даже попробовали это сделать. Но подробнее расскажем в одной из следующих статей.
Немного трушного R&D
Во время разработки и тестирования подобных алгоритмов, нужно собирать и обрабатывать много данных. Но передавать эти данные через бэкенд, или постоянно вставлять/доставать SD карту из IoT модуля не очень удобно. А ездить на самокате с ноутбуком в руках так вообще небезопасно. Поэтому мы решили прикрутить к самокату Raspberry Pi, а для полного комплекта еще и поставить камеру с сенсорным дисплеем.
Raspberry Pi</p>" data-abbr="RPi ">RPi запитывается прямо от самоката, так что не нужны никакие дополнительные источники питания. На RPi установлен Raspbian, Python и все необходимые библиотеки. При этом, IoT модуль по USB в режиме реального времени передает на RPi необходимые данные, которые сразу же обрабатываются, отображаются на графике и сохраняются в удобном формате на USB флешку.
Проверим формулу на деле
Учитывая всё вышеописанное и проделанное, кажется пора попробовать определить массу в реальной поездке, полученные данные на графике ниже.
Медианная масса по графику равна 129кг, что достаточно близко к реальности (масса пользователя около 100кг, а самоката примерно 32кг).
Для того, чтобы было чуть более понятно какие данные откуда берутся и куда передаются, ниже представлена блок-схема. На ней в качестве источников данных отмечены самокат и барометр, синими табличками представлены задачи, которые обрабатывают эти данные, а блоки в красных рамках - это кольцевые списки, которые содержат результаты работы задач-обработчиков.
Подведем промежуточный итог. Присутствует много неучтенных факторов и большое количество шумов при измерениях, но все же данные показывают, что массу пользователя с самокатом определять всё-таки можно, значит двигаемся дальше.
Проверка в поле
Возле офиса покатались, какую-то цифру получили, но данных, чтобы убедиться, что все работает приемлемо, все равно мало.
Нужно больше данных!
В нашем сервисе работает команда ребалансеров, чья задача — наводить порядок на парковках и доставлять самокаты на новые точки. Они всю смену ездят на самокатах. Для тестирования подобных алгоритмов они подходят, как никто другой. Поэтому мы попросили некоторых из них сообщить нам свой вес и пару дней передвигаться только на самокатах со специальной прошивкой.
Вот что из этого вышло:
На графиках данные четырех человек, каждый из которых отъездил на специальной прошивке по два дня. Почему все они весят больше 100кг, спросите вы? Стоит напомнить, что при помощи нашего алгоритма мы определяем вес не только пользователя, но и самоката. Модель, на которой ездили ребята, весит около 32кг. Мы сознательно не вычитали эту цифру из первоначальной формулы, так как моделей самокатов у нас представлено несколько, и по-хорошему, этот параметр нужно отдельно конфигурировать. Поэтому те 32кг, что весит самокат, вычитаем в уме и получаем вес человека. Что вышло в итоге? Судя по полученным распределениям, если брать медиану, то она +/- совпадает с реальным весом. Хотя форма распределения не является нормальной, а пик не такой уж и узкий. Впрочем, с этим может помочь дополнительная фильтрация промежуточных данных, уточнение экспериментально определенных коэффициентов и добавление неучтенных зависимостей между входными данными. Но не кажется ли вам, что и так сойдет?) Вот нам именно так и показалось, и мы решили отправить эту прошивку в город на несколько самокатов и посмотреть какие данные мы получим. К слову о данных…
Почему именно гистограмма?
Во время поездки значение веса измеряется с частотой примерно 10Гц, при этом измерения, даже не смотря на предварительную фильтрацию, получаются достаточно зашумленные. Поэтому, для того, чтобы получить, например, усредненное или медианное значение веса по итогам поездки, нам нужно либо считать его на лету, что не всегда возможно, либо собирать в один массив все измерения за поездку, а затем этот массив анализировать. Второй вариант интересней с точки зрения количества возможных характеристик, так как помимо медианы или среднего, имея весь массив измерений, мы можем получить любые статистические показатели. Однако, держать весь массив в памяти IoT модуля у нас нет возможности. А если передавать эти данные по мере получения на бэкенд, то по объему получаемых данных от самоката, измерения веса занимали бы больше места, чем вся остальная суммарная телеметрия. То есть для того, чтобы где-то эти данные складывать, нам понадобилось бы больше места, чем мы используем сейчас для всего, что приходит от самоката. В поисках золотой середины мы пришли к следующему решению:
-
на IoT модуле при старте поездки выделяется место под массив данных;
-
этот массив в дальнейшем будет содержать данные распределения измерений веса;
-
это распределение имеет вид гистограммы, у которой есть минимальная и максимальная границы, а также размер ячейки (если мин=0, макс=N, а размер ячейки=k, то количество элементов массива гистограммы будет N/k);
-
каждое измеренное значение увеличивает на 1 значение ячейки гистограммы в пределы которой попадает.
Таким образом, вместо массива исходных измерений с количеством элементов приблизительно равным 10*кол-во секунд поездки, мы получаем массив из значений ячеек гистограммы, размер которого конфигурируем сами.
А если представлять данные в формате таблицы, а не графиков, то разница выглядит вот так:
Такой подход позволяет нам сократить объем передаваемых и хранимых данных, но при этом сохранить необходимую информацию об измерениях.
Как выглядят нарушения?
Анализируя данные, которые мы собрали в городе на реальных поездках, по полученным гистограммам мы можем рассчитать медиану, среднее, квартильный размах и прочие показатели. Они используются для того, чтобы определить примерный вес пользователя который совершил данную поездку, а также сказать насколько мы уверены в полученной оценке.
Каждый объект собранных данных сохраняется в базе и содержит непосредственно массив данных гистограммы, ее параметры (верхнюю и нижнюю границы, размер ячейки), id пользователя (который совершил данную поездку), id устройства (на котором она была совершена) и данные о времени совершения поездки. Таким образом, собрав достаточное количество данных, мы можем получить и проанализировать совокупную гистограмму всех поездок, совершенных отдельно взятым пользователем или, например, на отдельно взятом самокате.
Ниже представлены графики с гистограммами двух разных пользователей.
Так, например, на графиках хорошего пользователя вы можете видеть, что вес его практически не меняется от поездки к поездке, причем все они совершены на разных самокатах. С большой долей вероятности это примерный пользователь, который постоянно ездит один.
А вот на графиках недобросовестного пользователя видно, что он скорее всего совершил несколько поездок в одиночку, так как вес по ним получился примерно одинаковый и при этом небольшой, а несколько поездок явно были двойными (или даже тройными).
То есть, если пользователь периодически совершает поездки, то в зависимости от веса, измеренного в каждой из них, можно сказать ездил ли этот пользователь с кем-то или может быть передавал самокат кому-то другому.
Что дальше?
В будущем сезоне мы планируем распространить этот алгоритм на все города присутствия Whoosh и продолжать его совершенствовать. Важно посмотреть эффективность наших расчетов в разных климатических, погодных и других условиях, а также на разных моделях самокатов. Также проверим взаимосвязь получаемых данных и общего состояния самоката (например, будут ли отличаться данные у нового и изношенного устройства, заряженного и разряженного?). Попробуем уточнить экспериментально полученные коэффициенты и выявить неучтенные зависимости. А потом еще разок все тщательно проверим. И главное — будем пробовать объяснять пользователям, почему все таки поездки вдвоем не безопасны, предлагать альтернативные решения для их задач.
Спасибо за внимание! Приятных поездок и ровных дорог! И не забывайте, один самокат = один человек)
Список источников
Теория автомобиля. Конспект лекций / А.Ш. Хусаинов, В.В. Селифонов – Ульяновск: УлГТУ, 2008.
Автор: Фарук Юссуф