В этом разделе мы подготовимся к работе с оборудованием телеметрии. Мы создадим сайт в котором будет отображаться тренд потока некоторых абстрактных случайным образом сгенерированных данных. Так как имеющиеся оборудование у каждого свое, со своими протокалами обмена (а может оборудование и отсутствует вовсе), мы сейчас не делаем упор на способы работы с ним, нам важно в общем выработать методику работы с потоками данных. По ходу дела мы продолжим изучение приемов программирования jCjS. Работа будет строится по этапам, фиксация каждого этапа в виде набора файлов хранится с соответствующей папке step[N].

Источник данных

Давайте создадим новый файл инициализации jCjS такого содержимого:

<?xml version="1.0" encoding="UTF-8"?>
<jCjS>
    <post
        objectName = "abstractTrend"
        >
        <handlers
            init = "stuff@examples/abstractTrend/init.js"
            cmd = "stuff@examples/abstractTrend/cmd.js"
        />
    </post>
</jCjS>

Детальное описание конфигурационного файла (см. раздел Конфигурация)

Командный скрипт:

http.respPlainText(src.value);

Некий внешний скрипт abstractTrendSrc.js:

//конструктор источника данных
AbsractTrendSrc = function() {
    //инкремент глобального идентификатора
    this.id++;
    //идентификатор таймера
    this.tid = 'timerAbsractTrendSrc' + this.id;
    //создаем и запускаем новый объект таймера поста
    post[this.tid] = new Timer();
    post[this.tid].timeout.connect(this, this.slotTimer);
    post[this.tid].start(1000);
}

//идентификатор объекта
AbsractTrendSrc.prototype.id = -1;

//текущее значение
AbsractTrendSrc.prototype.value = 0;

//функция обработки срабатывания таймера
AbsractTrendSrc.prototype.slotTimer = function() {
    this.value++;
}

И файл инициализации:

post.evalScript('stuff@examples/abstractTrend/abstractTrendSrc.js');
var src = new AbsractTrendSrc();

Тут нечто новое. В первой строке вызывается функция evalScript() глобального объекта post. В качестве аргумента указан путь к внешнему файлу скрипта abstractTrendSrc.js. Так в jCjS происходит вызов на исполнение внешнего скрипта, что является аналогом #include в С/С++ и аналогом включения внешнего скрипта в HTML страницах
<script language="javascript" type="text/javascript" src="/jsl/jquery.min.js"></script>

ЗАМЕЧАНИЕ: На самом деле язык JavaScript позволяет включить (выполнить) внешний скрипт и через функцию eval([текст скрипта]), но для того чтобы это сделать, нужно получить сам текст скрипта. Это можно сделать через функцию объекта post.loadScript([путь к скрипту]). Замените строку 1 в текущем init.js и убедитесь, что это работает.
eval(post.loadScript('stuff@examples/abstractTrend/abstractTrendSrc.js'));
Работает, но на самом деле эти две строки не эквивалентны, дело в последующей видимости функций и переменных объявленных во внешнем скрипте. Допустим вы во внешнем скрипте написали так:
var d1 = 'd1';//error
d2 = 'd2';//ok
function f1() { return 'f1'; }//error
var f2 = function() { return 'f2'; }//error
f3 = function() { return 'f3'; }//ok
Тогда после включения этого скрипта через eval(post.loadScript('...') никаких ошибок не будет, но если использовать post.evalScript('...') переменная d1 и функции f1, f2 - потеряют видимость. Попробуйте вызвать их из командного скрипта http.respPlainText(f1()). Но, вопрос, тогда зачем нужно вызывать через evalScript()? Все дело в отладчике скриптов, так уж устроен JavaScript, что через eval нельзя отладить внешний скрипт.
Резюме: во внешних скриптах всегда объявляйте глобально переменные для внешнего использования.

Продолжаем разговор. Во внешнем скрипте объявлен конструктор класса некоторого источника данных AbsractTrendSrc. Прототип этого класса содержит уникальный идентификатор id, который инкрементируется при каждом следующем вызове конструктора. Этот идентификатор нам необходим для создания уникального имени объекта поста - таймера. В строках 8-10 этот таймер создается и сигнал timeout() подключается к функции прототипа slotTimer. Обратите внимание, что если функция-обработчик является членом какого-либо класса, то в connect указаывается сначала указатель на сам класс this, а потом функция-обработчик и тоже через this.

Запускаем сервер, заходим на страницу, обновляем страницу. Должно все работать.

Очередь данных

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

/конструктор очереди данных
DataQueue = function() {}

//массив очереди данных
DataQueue.prototype.array = [];

//идентификатор последнего элемента array
DataQueue.prototype.lastId = -1;

//максимальный размер очереди данных
DataQueue.prototype.maxSize = 16;

//функция добавления нового элемента в очередь данных
DataQueue.prototype.append = function(value) {
    this.array.push(value);
    while(this.array.length > this.maxSize) { this.array.shift(); }
    this.lastId++;
}

//функция получения функции обновления удаленного массива
DataQueue.prototype.getFuncUpdate = function(lastId) {
    lastId = parseInt(lastId);
    if(isNaN(lastId)) { lastId = -1; }
    var str = '(function() {\r\n';
    str += 'array = array.concat([ ';
    var spos = this.array.length + lastId - this.lastId;
    if(spos < 0) { spos = 0; }
    for(var i = spos; i < this.array.length; i++) {
        str += this.array[i];
        if(i != this.array.length - 1) { str += ', '; }
    }
    str += ' ]);\r\n';
    str += 'while(array.length > ' + this.maxSize + ') { array.shift(); }\r\n';
    str += 'array.lastId = ' + this.lastId + ';\r\n';
    str += 'return array;\r\n';
    str += '});';
    return str;
}

Файл инициализации примет такой вид:

post.evalScript('stuff@examples/abstractTrend/abstractTrendSrc.js');
post.evalScript('stuff@examples/abstractTrend/dataQueue.js');
var src = new AbsractTrendSrc();
var queue = new DataQueue();

post.timerData = new Timer();
post.timerData.timeout.connect(funcTimerData);
post.timerData.start(1000);

function funcTimerData() {
    queue.append(src.value);
}

В нем происходит запуск процесса перекладывания данных с источника в очередь каждую секунду

JavaScript - удивительный язык! он может сам для себя написать функцию и потом ее же выполнить. Это обстоятельство позволяет оптимизировать синхронизацию двух массивов данных, один из которых расположен на сервере, другой расположен на браузере. Идея состоит в следующем: кроме самого массива на каждой стороне имеется идентификатор данных который увеличивается на 1 при каждом поступлении нового значения, изначально для пустого массива он равен -1. Так первый (с номером 0) элемент имеет идентификатор 0. Массив ограничен длиной maxSize, при превышении данного уровня на двух сторонах происходит подрезка массива до maxSize. Тем временем lastId растет неограниченно. Итак, для того чтобы клиенту получить последние данные с сервера он должен сообщить ему свой последний lastId. Сервер получая этот lastId, сравнивает его со своим lastId и высылает ему последние данные, еще он должен сообщить свой lastId относящийся к последнему элементу переданных данных. Чтобы на клиентской стороне не городить кучу кода, ответ с сервера высылается в виде текста функции обновления имеющиегося у него массива, вместе с lastId. Функция DataQueue.prototype.getFuncUpdate() и генерирует эту функцию по клиентскому lastId. Учтено, что клиент "проспал" целый цикл очереди или только подключился.

Запрограммируем командный скрипт так, чтобы на запрос с аргументом lastId он выдавал текст функции обновления клиентского массива, а без аргумента выдавал обычную HTML страницу, во всех остальных случаях выдается ошибка 404:

(function() {
    if(http.argCnt == 0 && http.pathCnt == 0) {
        http.respFile('stuff@examples/abstractTrend/tmp.html');
        return;
    }
    if(http.argKeyByNum(0) == 'lastId') {
        http.respPlainText(queue.getFuncUpdate(http.argByNum(0)));
        return;
    }
    http.respError(404);
    return;
})();

Соответствующая 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 language="javascript" type="text/javascript">
var array = [];
function requestData() {
   $.get('?lastId=' + array.lastId)
   .done(ansData)
   .error(ansError);
}
function ansData(ans) {
   eval(ans)(array);
   $('#data').html(array.toString());
   setTimeout(requestData, 1000);
}
function ansError() {
   $('#data').html('?');
   setTimeout(requestData, 1000);
}
$(requestData);

</script>

<title>Очередь данных</title>
</head>

<body>
<span id='data'></span>

</body>
</html>

В строке 8 объявлен локальный массив array. При первом запросе функции обновления в строке 10 lastId будет равен undefined - ничего страшного, функция очереди getFuncUpdate() для этого случая (lastId не приводимого к числу) сгенерирует ее так, что как будто lastId равен -1, то есть выдаст все имеющиеся данные. В строке 15, как и обещалось, массив array обновляется самым элегантным способом: вызов eval делает текст функции наcтоящей функцией, а последующие скобки с array сразу же приводят ее в действие. В строке 16 происходит индикация данных массива в элементе span. Запускаем сервер и наслаждаемся паровозиком данных длиной в 16 вагонов:
1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331

Сама функция, перехваченная заменой строки 16 на $('#data').html(ans), выглядит так:

(function() {
    array = array.concat([ 1575 ]);
    while(array.length > 16) { array.shift(); }
    array.lastId = 1574; return array;
});

Видно, что передается только одно новое значение 1575 вместо длинного массива.

Очередь данных во времени

Вообще данные телеметрии существуют во времени. Сейчас у нас такого нет и это легко исправить, для этого в файле инициализации init.js функцию funcTimerData() видоизменим так:

function funcTimerData() {
    queue.append('[' + new Date().getTime() + ', ' + src.value + ']');
}

Как известно JavaScript-у почти все равно, что текст, что числа, что массив, поэтому благодаря этой манипуляции у нас числовой массив заменился на массив массивов. Чтобы удобнее было рассмотреть результат этой манипуляции в tmp.html визоизменим функцию ansData таким образом:

function ansData(ans) {
   eval(ans)(array);
   var str = '';
   for(var i = 0; i < array.length; i++) {
       str += array[i][0] + ', ' + array[i][1] + '<br>';
   }
   $('#data').html(str);
   setTimeout(requestData, 1000);
}

В браузере наблюдаем такую картину

    1372270267123, 845
    1372270268123, 846
    1372270269123, 847
    1372270270123, 848
    1372270271123, 849
    1372270272123, 850
    1372270273123, 851
    1372270274123, 852
    1372270275123, 853
    1372270276123, 854
    1372270277123, 855
    1372270278123, 856
    1372270279123, 857
    1372270280123, 858
    1372270281123, 859
    1372270282123, 860

Вообще у нас почти все готово, чтобы нарисовать все это на графике. И забегая вперед, проблему перечачи потока строк - типа приемника GPS NMEA - в динамике мы уже решили.

Очередь данных на временном графике

Проведем следующие манипуляции: прежде всего, чтобы данные были действительно похожи на тренд, изменим тип функцию AbsractTrendSrc.prototype.slotTimer() в файле абстрактного источника данных следующим образом:

AbsractTrendSrc.prototype.slotTimer = function() {
    //this.value++;
    this.value += Math.random() - 0.5;
}

В математике и физике этот тренд называется траекторией "пьяного студента", чтобы увидеть тренд его движения до общежития на графике во времени передалаем 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 language="javascript" type="text/javascript" src="/jsl/flot/jquery.flot.min.js"></script>
<script language="javascript" type="text/javascript">
var array = [];
function requestData() {
   $.get('?lastId=' + array.lastId)
   .done(ansData)
   .error(ansError);
}
function ansData(ans) {
   eval(ans)(array);
    setTimeout(requestData, 1000);
}
function ansError() {
   setTimeout(requestData, 1000);
}
function plotData() {
    $.plot($('#data'),
    [
        {
            data: array,
            color: "#FF0000"
        }
    ],
    {
        series: {
            shadowSize: 0
        },
        xaxis: {
            timeformat: "%H:%M:%S",
            mode: "time",
            min: new Date().getTime() - 101*1000,
            max: new Date().getTime()
        }
    }
    );
}
$(function() { requestData(); setInterval(plotData, 100); });

</script>

<title>Очередь данных</title>
</head>

<body>
График тренда
<br><br>
<div id="data" style="height:480px;width:100%"></div>

</body>
</html>

Что можно доделать?

Наше приложение работает, но не лишено множества недостатков. Приведем список дел, которые Вы можете воспринимать как домашнее задание:

Далее