В этом разделе мы подключим приемник GPS к серверу jCjS. Это устройство доступно и широко распространено, и поэтому именно этот тип устройств был выбран для начала описания концепций взаимодействия jCjS c реальным оборудованием по последовательному порту. Данный материал относится к операционной системе Linux, реализация этого же приложения jCjS в Windows не должно составлять проблемы.

Подключение GPS Bluetooth приемника в терминале Linux

Bluetooth GPS приемники предоставляют данные через виртуальный последовательный порт. Для того, чтобы такой порт появился и могли бы из него читать (писать в GPS приемник ничего не надо) необходимо запарить компьютер с GPS приемником и добится появления виртуального порта в каталоге /dev. Делать мы это будем через команды консоли и правку конфигурационных файлов.

Включим GPS приемник и попробуем его обнаружить:

jury@tr:~$ hcitool scan
Scanning ...
   78:1F:DB:46:99:D5    n/a
   00:0D:B5:36:EF:17    BT-GPS-36EF17
jury@tr:~$

Первая строка в найденном - это телефон соседа, а вторая, кажется то, что надо. Теперь свяжем это устройство с именем виртуального порта rfcomm0 и каналом 1. Команда должна идти от суперпользователя:

jury@tr:~$ sudo rfcomm bind rfcomm0 00:0D:B5:36:EF:17 1

Пробуем читать

jury@tr:~$ sudo cat /dev/rfcomm0

В терминале должны начать выскакивать строки NMEA:

...

Скорее всего воспользоваться портом /dev/rfcomm0 без прав суперпользователя тоже не получится, так как

ls -l /dev
...
crw-rw----  1 root dialout 216,   0 июля   4 11:58 rfcomm0
...

прав у обычного пользователя на это нет. К тому же, если мы перезагрузим компьютер, то порт созданный командой rfcomm bind исчезнет, и его надо будет заново связывать. Нам это не удобно, поэтому сейчас все сделаем на постоянной основе, начнем с конфигурационного файла связки порта.

jury@tr:~$ sudo gedit /etc/bluetooth/rfcomm.conf

доводим файл до такого состояния:

#
# RFCOMM configuration file.
#

rfcomm0 {
   # Automatically bind the device at startup
   bind yes;

   # Bluetooth address of the device
   device 00:0D:B5:36:EF:17;

   # RFCOMM channel for the connection
   channel   1;

   # Description of the connection
   comment "GPS Bluetooth device";
}

Также добавим jury в группу dialout:

jury@tr:~$ sudo gedit /etc/group

...
kmem:x:15:
dialout:x:20:jury
fax:x:21:
...

Перезагружаемся, проверяем что все работает:

cat /dev/rfcomm0

Вообще Bluetooth есть Bluetooth, к сожалению, похоже недостатков у этого интерфейса больше чем достоинств. Дело даже не в сложности и непрозрачности настройки, дело в том, что открытие уже правильно настроенного порта может занять непредсказуемое время и программа будет висеть в ожидании, может пройти 10-15-20 секунд, прежде чем что-то произойдет, и то, что произойдет есть не всегда успешное открытие. Вывод, если есть выбор между проводным и беспроводным устройством - лучше проводное.

Транслируем строки NMEA через интернет

Мы увидели строки вываливающиеся из приемника GPS как из рога изобилия. Они содержат информацию о положении, скорости, направления (курса), точного времени, количества и положения спутников. Cейчас устроим трансляцию последней полученной строки NMEA в интернет с помощью jCjS.

Начнем с файла инициализации поста.

//строка ответа NMEA
var ansDev = 0;

//открываем порт GPS приемника
post.serial = new SerialPort();
post.log('открываем порт /dev/rfcomm0 ...', 1);
if(!post.serial.open('/dev/rfcomm0')) {
    post.log('ошибка открытия порта /dev/rfcomm0', -1);
} else {
    post.log('порт /dev/rfcomm0 открыт', 1);
}

//счетчик ответов
var ansCnt = 0;

//отработчик поступления строк в порт GPS приемника
post.serial.readyRead.connect(readyRead);
function readyRead() {
    while(post.serial.canReadLine()) {
        var str = post.serial.readLine();
        if(str.slice(0, 6) !== '$GPRMC') { continue; }
        ansDev = str;
        if(ansCnt < 10) {
            post.log(ansDev, 1);
        }
        ansCnt++;
    }
}

В этом скрипте мы видим две новых функции. Первая - это функция встроенного глобального объекта post new SerialPort(), по синтаксису и применению она очень похожа на функцию создания таймера new Timer(). В строке 3 мы открываем наш виртуальный порт /dev/rfcomm0 и выдаем лог, через, также встроенную в post функцию log(). Первый аргумент log() - текст сообщения, второй аргумент уровень важности лога. Если он выше или равен текущего установленного порога важности логов, а он у нас по умолчанию равен 1 (см. лог запуска сервера 2013-06-29-10:48:53.858 v0 obj=server jcjshttpserver.cpp:340 init() порог логирования 1). Если уровень важности лога отрицателен, то он всегда выдается и означает ошибку.

Мы используем виртуальный последовательный порт и его параметры: скорость, количество бит, проверка четности - не используются, точнее они не влияют на работу Bluetooth порта. Счастливые обладатели GPS приемника работающего по "настоящему" последовательному порту могут не мучится настройкой Bluetooth, но им будет необходимо выставить параметры порта до (или после его) успешного открытия, примерно так:

post.serial.baudRate = 9600;
post.serial.parity = 0;
post.serial.stopBits = 2;
post.serial.dataBits = 8;

Подробное описание свойств и функций класса последовательного порта смотрим в ...

При поступлении новых данных в буфер объекта последовательного порта он генерирует сигнал readyRead(), который мы перехватываем в нативной функции JavaScript readyRead(). В ней мы проверяем, что поступила целая строка (или пакет строк), далее мы изымаем данные построчно в цикле while с помощью функций объекта последовательного порта canReadLine() и readLine(). Также мы выдаем 10 прочитанных строк в лог, уведомляя журнал, что процесс чтения данных NMEA GPS приемника запущен нормально. Строка 14 фильтрует строки с записью GPRMC, так как только этот тип строк содержит информацию о местоположении и времени.

Пока совершенно тривиальный командный скрипт:

http.respPlainText(ansDev);

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

$GPRMC,092618.000,A,5505.3114,N,03847.1453,E,0.13,131.12,290613,,*03

Сообщаем географические координаты: 55° 5 минут северной широты; 38° 47 минут восточной долготы, время по Гринвичу: 9 часов 26 минут 18 секунд. Полет нормальный.

Парсин строк NMEA

Создадим скрипт создания объекта парсинга строк NMEA nmea.js с таким содержанием и согласно стандарту NMEA:

//парсим строку http://ru.wikipedia.org/wiki/NMEA

NmeaParser = function() {}
NmeaParser.prototype.gprmc = '';
NmeaParser.prototype.time = new Date();
NmeaParser.prototype.lat = 45;
NmeaParser.prototype.lng = 0;
NmeaParser.prototype.actual = false;

NmeaParser.prototype.setNmeaStr = function(str) {
    if(str.slice(0, 6) != '$GPRMC') { return; }
    var precision = 100000;

    this.gprmc = str;
    var listData = str.split(",");
    this.actual = (listData[2] === 'A');
    if(!this.actual) { return; }

    //время и дата
    if(listData[1].length != 0 && listData[9].length != 0) {
        this.time.setMonth(parseInt(listData[9].slice(2, 4), 10) + 1, listData[9].slice(0, 2));
        this.time.setFullYear('20' + listData[9].slice(4, 6));
        this.time.setHours(listData[1].slice(0, 2));
        this.time.setMinutes(listData[1].slice(2, 4));
        this.time.setSeconds(listData[1].slice(4, 6));
        this.time.setMilliseconds(listData[1].slice(7, 3));
    } else {
        this.time = undefined;
    }

   //широта
   if(listData[3].length != 0) {
       var latGrad = listData[3].slice(0, 2);
       var latMin = listData[3].slice(2);
       var latSign = listData[4] == "N" ? +1 : -1;
       this.lat = parseInt(precision * latSign * (parseInt(latGrad, 10) + parseFloat(latMin / 60)) + 0.5) / precision;
   } else {
       this.lat = undefined;
   }

    //долгота
    if(listData[5].length != 0) {
        var lngGrad = listData[5].slice(0, 3);
       var lngMin = listData[5].slice(3);
       var lngSign = listData[6] == "E" ? +1 : -1;
        this.lng = parseInt(precision * lngSign * (parseInt(lngGrad, 10) + parseFloat(lngMin / 60)) + 0.5) / precision;
    } else {
        this.lng = undefined;
    }
}

Нам пока не нужны все параметры - только широта, долгота и время. Впоследствии этот скрипт будет дополнен. Также видоизменим файл инициализации так, чтобы полученная строка от приемника передавалась объекту nmea, а командный скрипт выдавал распарсенную строку в виде литерала объекта.

//объект парсинга GPS
post.evalScript('stuff@examples/gps/nmea.js');
var nmea = new NmeaParser();

//строка ответа NMEA
var ansDev = 0;

//открываем порт GPS приемника
post.serial = new SerialPort();
post.log('открываем порт /dev/rfcomm0 ...', 1);
if(!post.serial.open('/dev/rfcomm0')) {
   post.log('ошибка открытия порта /dev/rfcomm0', -1);
} else {
    post.log('порт /dev/rfcomm0 открыт', 1);
}

//счетчик ответов
var ansCnt = 0;

//отработчик поступления строк в порт GPS приемника
post.serial.readyRead.connect(readyRead);
function readyRead() {
   while(post.serial.canReadLine()) {
      var str = post.serial.readLine();
      if(str.slice(0, 6) !== '$GPRMC') { continue; }
      ansDev = str;
      if(ansCnt < 10) {
          post.log(ansDev, 1);
       }
       ansCnt++;
       
       nmea.setNmeaStr(str);
      if(!nmea.actual) { continue; }
   }
}

Командный скрипт пусть выдает данные времени, широты, долготы в виде:

var str = '({ ' +
    'time: ' + nmea.time.getTime() + ', ' +
    'lat: ' + nmea.lat + ', ' +
    'lng: ' + nmea.lng + ' })';
http.respPlainText(str);

Страница должна выглядеть так:

({ time: 1378281361000, lat: 55.08839, lng: 38.78585 })

Отображаем координаты на карте

Сейчас мы уже сделаем нечто серьезное, а именно: воспользуемся текущими наработками и картами google для отображения своего местоположения на карте. Для этого подкорректируем командный файл так, чтобы на запрос без аргументов была выдана HTML страница, а на запрос с аргументом pos выдавался текст литерала объекта текущей позиции.

(function() {
    if(http.argCnt === 0 && http.pathCnt === 0) {
        http.respTmplFile('stuff@examples/gps/step_2/tmp.html');
        return;
    }
    if(http.argKeyByNum(0) === 'pos') {
        var str = '({ time: ' + nmea.time.getTime() + ', ' +
            'lat: ' + nmea.lat + ', ' +
            'lng: ' + nmea.lng + ' })';
        http.respPlainText(str);
        return;
    }

    http.respError(404);
})();

Обратите внимание, что текст HTML страницы выдается функцией respTmpFile(), а не функцией respFile(), это значит, что в странице имеются шаблонные JavaScript вставки. Действительно, в 33 строке мы видим

var myLatlng = new google.maps.LatLng(<!--= nmea.lat-->, <!--= nmea.lng -->);

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

var myLatlng = new google.maps.LatLng(55.08815, 38.78611);

Итак, вот текст файла html страницы:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<script language="javascript" type="text/javascript" src="/jsl/jquery.min.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script language="javascript" type="text/javascript">
var pos;
var map;
var marker;

function requestData() {
   $.get('?pos')
   .done(ansData)
   .error(ansError);
}
function ansData(ans) {
   pos = eval(ans);
   $('#pos').html(pos.lat + ', ' + pos.lng);

   var myLatlng = new google.maps.LatLng(pos.lat, pos.lng);
   marker.setPosition(myLatlng);

    setTimeout(requestData, 1000);
}
function ansError() {
   setTimeout(requestData, 1000);
}

function initialize() {

    var myLatlng = new google.maps.LatLng(<!--= nmea.lat-->, <!--= nmea.lng -->);

    var myOptions = {
        zoom: 18,
        center: myLatlng,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    }
    map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

    marker = new google.maps.Marker({
        position: myLatlng,
        map: map,
        title:"My position"
    });

    requestData();
}
$(initialize);

</script>

<title>GPS</title>
</head>
<body>

Мое текущее расположение:
<span id="pos"></span>
<div id="map_canvas" style="height:480px; width:90%"></div>

</body>
</html>

Выглядит все это примерно так:

В продолжение темы

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

Ссылки на используемые ресурсы