Pull to refresh

Передача GPS-трека по SMS

Reading time6 min
Views12K
У вас прогрет распределённый и отказоустойчивый бэкенд, написано крутое мобильное приложение под все возможные платформы, но, внезапно, выясняется, что ваши пользователи так далеки от цивилизации, что единственный способ связи с ними — это SMS? Тогда вам будет интересно прочитать историю о том, как передать максимум информации, используя этот архаичный канал для передачи данных, на примере GPS-трека.

Немного про GPS-трек


В нашем конкретном случае под треком подразумевалась последовательность точек, задаваемых координатами, в которых находился пользователь, с привязкой ко времени и, возможно, некоторыми флагами (например, SOS, начало трека).

Время можно было определять таким способом:

long time= System.currentTimeMillis();

Координаты — это долгота и широта. В объекте Location в Android используется double для longitude и latitude.

Таким образом, каждая точка трека должна была содержать в себе 64 + 2 * 64 + 2 = 194 бита информации.

Немного про SMS


Байка про SMS
В нулевые в студенческие годы на базах отдыха за пределами города проходили всевозможные зимние выездные школы и молодёжные конференции. В ту пору надёжное покрытие сотовой связи за городом было редкостью, а своим родителям как-то надо было сообщить, что жив-здоров, не замёрз и шапку не забыл. Вот тогда-то и приходило понимание, что SMS'ки вещь нужная и полезная. Отправить SMS с земли не получалось, а вот на некоторой высоте — на второй-третий раз удавалось. Надёжнее всего было привязать телефон к длинной палке, набрать и отправить сообщение, поднять палку вверх, подержать её некоторое время вертикально, затем проверить — отправилось ли сообщение, в случае необходимости — повторить процедуру. Некоторые особо отчаянные подбрасывали телефоны вверх в надежде отправить таким образом SMS'ку — сообщения-то отправлялись, да только потом приходилось искать свою Nokia в сугробе. На моей памяти ни один телефон не потеряли, не разбили — Nokia же.

Кто пользовался (пользуется) SMS помнит, что кириллицей можно набирать 70 символов в одну SMS, а транслитом — целых 160! Вот где несправедливость, а вы говорите санкции.

О том, как же устроена SMS'ка можно почитать в RFC 5724:
GSM SMS messages are alphanumeric paging messages that can be sent to
and from SMS clients. SMS messages have a maximum length of 160
characters (7-bit characters from the GSM character set [SMS-CHAR]),
or 140 octets. Other character sets (such as UCS-2 16-bit
characters, resulting in 70-character messages) MAY also be supported
[SMS-CHAR], but are defined as being optional by the SMS
specification. Consequently, applications handling SMS messages as
part of a chain of character-processing applications MUST make sure
that character sets are correctly mapped to and from the character
set used for SMS messages.

Таким образом, в качестве полезной нагрузки можно использовать 140 байт, которые превращаются в те самые 160 символов для 7-битной кодировки (латиница, цифры и ещё некоторое количество символов).

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

В теории поддерживается разбиение до 255 сегментов, на практике же видел гарантированную поддержку только 6 сегментов.

Бинарный формат: Заголовок


Для помещения бинарных данных трека в SMS должно было быть использовано простое и совместимое с 7-битной кодировкой Base64 преобразование, которое на выходе на каждые три байта исходных данных выдаёт четыре 7-битных символа. Итого, полезных данных остаётся уже не так много — всего-то 160 * 3 / 4 = 120 байт.

Приложению пророчилось большое будущее, поэтому разрабатываемый формат не должен был ограничиваться как одним типом сообщения, так и одной версией протокола, поэтому под messageType был отведён тип short.

Поскольку данные должны были поступать по SMS, где нет привычных для классического web-а сертификатов, паролей и аутентификаций, нужно было научиться привязывать получаемое сообщение к пользователю системы: был введён authenticationToken типа long, генерируемый случайным образом.

Для контроля целостности было добавлено поле с контрольной суммой checksum типа short.

Общий размер заголовка стал равен 2 + 8 + 2 = 12 байтам.

Исключим из общего объёма полезных данных размер заголовка: 120 — 12 = 108 байт или
864 бита.

Наивная реализация


Одна точка трека занимает 194 бита. В одну SMS'ку входит 864 бита данных трека. Кхм, влезет лишь 864 / 194 = 4 точки.

А что если разбить сообщение на 6 сегментов? 1 сегмент = 153 7-битных символа; 6 сегментов = 153 * 6 = 818 7-битных символа. Полезных данных окажется 818 * 3 / 4 = 613 байт. Таким образом, под данные трека останется 613 — 12 = 601 байт или 4808 бита. Итого, в жирную SMS'ку можно будет засунуть 4808 / 194 = 24 точки и ещё 19 байт останется.

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

Оптимизация


Время


  1. В действительности нам не нужна точность в миллисекунду
  2. Приложение не будет отсылать данные за прошедшие десятилетия
  3. Приложение вряд ли просуществует в неизменном виде 50 лет

Пусть мы оставим точность до 4 секунд.

Введём свою эру:

long ERA = 1388534400000L;

Относительно стандартной юниксовой (1 января 1970 года UTC). Дата выше соответствует 1 января 2014 года UTC.

С учётом последнего допущения нам достаточно хранить 4 байта для времени:

(60 / 4) * 60 * 24 * 365.25 * 50 = 394 470 000 < 2^29. Ещё 3 бита в запасе (на флаги).

long time= System.currentTimeMillis();
long newTime = (time - ERA) / (1000 * 4);

География


Земля очень близка по форме шару, сплюснутому со стороны полюсов, таким образом длина экватора (40 075 696 метров) чуть больше, чем длина удвоенного меридиана (40 008 552 метров).

Координаты в Location задаются в градусах (± в зависимости З/В и С/Ю).

Всего же в окружности у нас 360 градусов, или 21 600 минут или 1 296 000 секунд. Таким образом, в одном метре экватора или меридиана не менее 0.032 секунды (1 296 000 / 40 075 696 = 0.0323388...). На широте, скажем, 60 градусов в одном метре параллели будет примерно в 2 раза больше секунд (около 0.064 секунды). Что это значит? Ошибка позиционирования в 1 метр на экваторе и на 60-ой параллели отличается в ошибке в градусах Location.getLongitude() в два раза. При этом, чем дальше от экватора, тем ошибка в градусах при фиксированной в метрах выше. И, наоборот: при удалении от экватора при фиксированной ошибке в градусах — в метрах ошибка уменьшается, то есть вблизи экватора округление до 32/1000 секунды даст наибольшую ошибку позиционирования не более одного метра.

Допустим, нас устраивает точность позиционирования в 5 метров (в действительности, точность значений, получаемого от GPS-модуля оказывается в разы хуже). Возьмём границу пониже: пусть по широте и долготе точность позиционирования не менее 3 метров (5 > 3 * sqrt(2)).

Теперь, мы можем отказаться от координат в double и привести их к неотрицательному целочисленному значению с точностью до 96/1000 секунды:

long newLatitude = (latitude + 90) * 60 * 60 * 1000 / 96;
long newLongitude = (longitude + 180) * 60 * 60 * 1000 / 96;

Очевидно, что новые значения не превысят 360 * 60 * 60 * 1000 / 96 = 13 500 000 < 2^24, что укладывается в 3 байта, да ещё и бит остался «про запас» от преобразования latitude, так как максимально возможное значение будет меньше в 2 раза и для хранения значения будет достаточно 23 бита.

Результат


Размер точки трека сократился до 4 + 3 + 3 = 10 байт и ещё несколько битов осталось не использованных. В обычную SMS стало входить 864 / 80 = 10 точек. В жирную из 6 сегментов: 4808 / 80 = 60 точек.

Ещё оптимизации


До этого мы никак не использовали то, что точки принадлежат одному треку, следовательно можно делать предположения о расстоянии между точками и временных промежутках.

Таким образом, абсолютные координаты и время фиксировались только для первой точки, а все последующие точки хранили в себе смещение относительно предыдущей и по координатам, и по времени. Такая оптимизация позволила сократить размер последующих точек ещё на 2 байта до 8 байт, увеличив, в итоге, общее количество точек в одной SMS'ке до 13, а в жирной — до 84.

Описание формата


HEADER (96) + BODY (variable):
|| Message Type (16) | Authentication Token (64) | Checksum (16) || Data (variable) ||

FIRST TRACKPOINT INFO (80):
|| Start (1) | SOS (1) | Reserved (1) | DateTime (29) | Reserved (1) | Latitude (23) | Longitude (24) ||

NTH TRACKPOINT INFO (64):
|| Offset (16) | Start (1) | SOS (1) | North (1) | Latitude (21) | Reserved (1) | Reserved (1) | East (1) | Longitude (21) ||

В скобках указаны длины полей в битах.

Пример


HEX-запись бинарного сообщения (30 байт), состоящего из заголовка (12 байт) и двух точек (10 и 8 байт, соответственно):

00 01 00 11 AA BB CC DD EE FF 00 90 80 00 24 09 54 04 9D 89 87 A0 09 B1 40 00 00 20 92 7C

Расшифровка:

short messageType = 1; // 00  01
long authenticationToken = 4972798176784127L; // 00  11  AA  BB  CC  DD  EE  FF
short checksum = 144; // 00  90
boolean start = true; // [1]000 0000  00  24  09
boolean sos = false; // 1[0]000 0000  00  24  09
int dateTime = 9225; // 100[0 0000  00  24  09] // 1 января 2014 10:15:00 UTC
int latitude = 5506205; // 0[101 0100  04  9D] // 56.83213333° с. ш. (исходные данные: 56.832139° с. ш.)
int longitude = 9013152; // 89  87  A0 // 60.35072° в. д. (исходные данные: 60.350722° в. д.)
int offset = 2481 // 09  B1 // 1 января 2014 12:45:24 UTC
boolean start2 = false; // [0]100 0000  00  00
boolean sos2 = true; // 0[1]00 0000  00  00
boolean north2 = false; // 01[0]0 0000  00  00
int latitude2 = 0; // 010[0 0000  00  00] // 56.83213333° с. ш.
boolean east2 = true; // 00[1]0 0000  92  7C
int longitude2 = 37500; // 001[0 0000  92  7C] // 61.35072° в. д.

P.S. Всех с наступающим! Надеюсь, пост окажется кому-нибудь полезным/интересным.
Tags:
Hubs:
+12
Comments27

Articles

Change theme settings