http://habrahabr.ru/post/266317/
Недавно я опубликовал статью
«Перенаправление данных из COM-порта в web», в которой описал прототип системы, транслирующей строки из последовательного порта компьютера в веб-браузер. В той статье я указал направления, в которых надо доработать прототип, чтобы приблизить его к продакшен-стадии:
— никакой дизайн веб-страницы
— в каждый момент времени данные получит только один веб-клиент
— очень ограниченный набор браузеров, с помощью которых можно получить доступ. Например, не работает ни в Internet Explorer 8, ни в браузере из Android 2.3.5
— требуется установка python
Через некоторое время я решил не оставлять его в таком виде и доработать. Под катом результат доработки и описание того, как я устранил все перечисленные недостатки.
Сразу покажу итоговый результат:
На этом видео видно, что строки из COM-порта отображаются сразу в трёх браузерах одновременно: в Firefox и IE 8 на том же компьютере, к которому подключена Arduino, и на смартфоне.
Сама Arduino передаёт строку «Температура: XXXXXX», а строку с датой-временем и номером строки — одна из частей бэкенда.
Теперь по порядку о том, как устранялись недостатки. В этот раз я постараюсь не быть очень нудным и не описывать каждую строчку кода, а показать только основные моменты. Если появятся вопросы, задавайте в комментариях.
Плохой дизайн веб-страницы
В предыдущей статье я писал, что в создании веб-фронтенда я разбираюсь от слова «никак», поэтому создание нормальной веб-страницы было для меня самым сложным. К счастью я почти сразу же обнаружил сайт
w3schools.com, на котором открыл для себя Bootstrap и нашёл хорошие учебники по Ajax и jQuery. Для матёрых фронтенд-разработчиков представленные на этом сайте учебники, наверное, вызовут только улыбку, но для таких новичков, как я, это самое то, что надо.
Самое приятное в этих учебниках то, что они очень небольшие и очень по существу. Без размазывания каши по тарелке. В принципе, одного вечера на изучение достаточно, чтобы начать что-то делать.
Оказалось, что с помощью Bootstrap наваять более-менее приемлемый дизайн веб-страницы — это не так уж и сложно. Он, конечно, не сделает из вас Артемия Лебедева, но
запрограммировать интерфейс с пользователем сделать на нём можно очень быстро.
Единственное, что я не смог понять, как с его помощью сделать нужное мне разбиение страницы на две части: большую, в которой текст отображается по середине в вертикальной плоскости, и маленькую, которая всё время «прижата» к нижнему краю окна браузера. Но тут на помощь пришла статья
«Vertical Centering in CSS». В результате получилась вот такая заготовка под веб-страницу:
<!-- Vertical aligment of text from "Vertical Centering in CSS" at
http://www.jakpsatweb.cz/css/css-vertical-center-solution.html -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<style type="text/css">
html, body {
height: 100%;
margin: 0px;
}
</style>
</head>
<body>
<div style="display: table; height: 90%; width: 100%; overflow: hidden;">
<div style=" display: table-cell; vertical-align: middle;">
<div style="text-align: center">
data place<br>
пока нет данных
</div>
</div>
</div>
<div style="display: table; height: 10%; width: 100%; overflow: hidden;">
<div style=" display: table-cell; vertical-align: middle;">
<div style="text-align: right;">
buttons
</div>
</div>
</div>
</body>
</html>
Практически весь остальной код страницы получился после чтения учебников по Bootstrap и JavaScript (раздел JS HTML DOM). Исключение — это код jQuery, который обновляет данные на странице. Но об этом чуть позже.
Ограниченный набор поддерживаемых браузеров
Слабая поддержка браузерами предыдущего прототипа была обусловлена выбранной технологией доставки обновлений информации: Server-Sent Events. Поэтому в этот раз я решил воспользоваться старой проверенной временем технологией Ajax. Использование Ajax-а приводит к увеличению веб-трафика, но зато, по-моему, должно работать в максимальном количестве браузеров.
Ещё одним недостатком Ajax-а можно считать тот факт, что, если не предпринять никаких специальных мер, то возможны пропуски строк, которые передаются через COM-порт: если строки в последовательный порт будут поступать очень быстро, а Ajax-запросы приходить реже, что все строки между запросами будут не видны клиенту. Но для задачи отображения, например, текущей температуры — это совсем не страшно.
Можно было бы, наверное, воспользоваться технологией WebSockets, но, насколько я понял, в IE она поддерживается только с 10-й версии, а у меня IE 8, поэтому я даже не стал прорабатывать это направление.
Ещё буквально на днях в какой-то из статей на хабре или гиктаймс я наткнулся на упоминание библиотеки
SockJS, которая, похоже, умеет обходить отсутствие WebSockets, но, во-первых, для неё требуется спец. поддержка на стороне сервера, а, во-вторых, у меня к этому моменту нормально работал Ajax, поэтому и она осталась без моего внимания.
Итак, Ajax. Когда-то давно я пытался изучить эту технологию. Но все бумажные учебники, которые мне попадались, были слишком нудными и я очень быстро бросал это дело. А вот на уже упомянутом
w3schools.com учебник оказался очень хорошим. В результате достаточно быстро получился следующий код:
function get_data()
{
var xmlhttp;
xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET","/get_serial?r=" + Math.random(),true);
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("data").innerHTML=xmlhttp.responseText;
get_data();
}
}
xmlhttp.send();
}
который вызывался при окончании загрузки страницы:
<body onload="get_data()">
В этом коде надо, наверное, обратить внимание на два момента. Во-первых, на строчку
xmlhttp.open("GET","/get_serial?r=" + Math.random(),true);
Именно в ней происходит обращение к веб-серверу за очередной строкой из COM-порта. Добавка
r=" + Math.random()
нужна для того, чтобы Internet Explorer не кэшировал ответы. В противном случае он пошлёт только один Ajax-запрос, получит ответ и больше уже не будет обращаться к серверу. В интернете я видел решения проблемы кэширования путём посылки со стороны сервера специальных HTTP-заголовков
response.headers['Last-Modified'] = datetime.now()
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1'
но у меня это почему-то не заработало.
Альтернативный вариант — это использование POST вместо GET. Говорят, что такие запросы IE не кэширует, но я не проверял.
Кстати, jQuery, использует точно такой же способ борьбы с кэшированием — он к УРЛ-у тоже добавляет случайную строку. Только выглядит она несколько иначе.
Второй момент — это последовательность строк
xmlhttp.open("GET","/get_serial?r=" + Math.random(),true);
и
xmlhttp.onreadystatechange=function()
...
Изначально они стояли в другом порядке. И в Internet Explorer-е это приводило к странным проблемам — он выжирал всю доступную память. И только после этого обновлял данные. На чём и заканчивал свою работу.
Только на сайте
XmlHttpRequest.ru я нашёл, что при повторном использовании объекта XMLHttpRequest рекомендуется сначала вызвать метод open() и только после этого менять свойство onreadystatechange.
Получив столько проблем с IE на ровном месте, я решил, что надо бы воспользоваться библиотекой, в которой подобные нюансы скорее всего уже учтены. Поскольку для Bootstrap-а на странице уже загружался jQuery
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
я решил, что негоже не воспользоваться поддержкой Ajax, встроенной в эту библиотеку. Поэтому код обновления данных преобразовался в такой:
function on_load(responseTxt, statusTxt, xhr){
if( statusTxt == "success" ){
$("#data").load("/get_serial?r=" + Math.random(), on_load )
} else {
$("#data").text( "Error: " + xhr.status + ": " + xhr.statusText );
}
}
$(document).ready(function(){
on_load("", "success", 3);
});
Один клиент в каждый момент времени
В предыдущей статье я уже писал про возможный путь решение этой проблемы: разбиение бэкенда на две части, использование ZMQ для их связи + многопользовательский http-сервер в качестве второй части. В качестве такого сервера я выбрал Flask. Я не проводил никаких сравнений альтернатив, он просто попался мне первым. Выяснилось, что его можно запустить в режиме параллельной обработки http-запросов и этого оказалось достаточным. Для запуска такого режима достаточно передать ему параметр
threaded = True (см.
stackoverflow.com/questions/14672753/handling-multiple-requests-in-flask):
app.run(host='0.0.0.0', debug=False, threaded=True)
Помимо многопоточности Flask предоставляет ещё очень удобный механизм маршрутизации, т.е. соответствия отдельных процедур определённым http-запросам. Так что для создания простеньких веб-приложений Flask — очень милое дело.
Но больше всего мне хотелось бы рассказать про библиотеку ZMQ — продемонстрировать её потрясающие возможности.
Вот код демонстрационного ZMQ-сервера:
import zmq
import time
context = zmq.Context.instance()
pub_sock = context.socket(zmq.PUB)
pub_sock.bind( 'tcp://*:12345' )
count = 0
def get_full_line_from_serial():
global count
count += 1
return time.strftime( '%Y.%m.%d %H:%M:%S' ) + ': string N %d' % count
while True:
line = get_full_line_from_serial()
print line
pub_sock.send( line )
time.sleep(1)
который имитирует чтение из COM-порта. На самом деле он каждую секунду генерирует новую строку, «публикует» её (т. е. отсылает всем подписавшимся клиентам) и для отладки выводит её на печать.
А вод код клиента:
import zmq
context = zmq.Context.instance()
zmq_sub_sock = context.socket(zmq.SUB)
zmq_sub_sock.setsockopt(zmq.SUBSCRIBE, '')
zmq_sub_sock.connect( 'tcp://localhost:12345' )
poller = zmq.Poller()
poller.register( zmq_sub_sock, zmq.POLLIN )
while True:
socks = dict(poller.poll(timeout=5000))
if zmq_sub_sock in socks:
print zmq_sub_sock.recv()
else:
print 'No data within 5 sec'
который открывает ZMQ-сокет, подписывается на все возможные сообщения и подключается к серверу. После чего в бесконечном цикле ожидает новых данных и выводит их.
А вот демонстрация их совместной работы:
На что хочется обратить внимание. Во-первых, клиента можно запустить до запуска сервера. И это никак не скажется на его работоспособности. Во-вторых, даже падение сервера не приведёт к падению клиентов — как только сервер перезапустится, клиенты снова начнут получать сообщения от него. Ну не фантастика ли? И это при полном отсутствии каких-либо специальных инструкций в исходных кодах клиента и сервера. Сколько кода пришлось бы написать самостоятельно для реализации подобного функционала при использовании обычных TCP/IP сокетов?
И это только маленькая толика того, что умеет библиотека ZMQ. В очередной раз крайне настоятельно рекомендую присмотреться к этой библиотеке.
Standalone приложение
Я уже, вроде бы, упоминал, что python слабо приспособлен к созданию standalone приложений. Это интерпретируемый язык, для обычной работы которого требуется программа-интерпретатор. К сожалению для него не существует компилятора, который бы умел генерировать нативный бинарный код. Мне известны две программы, позволяющие создать из скрипта некое подобие самостоятельного приложения: py2exe и pyInstaller. Сам я чаще всего пользуюсь вторым, поэтому и в данном проекте решил использовать его.
pyInstaller берёт питоновский скрипт, анализирует все файлы, от которых он зависит (определяет все импортируемые модули и подключаемые динамические библиотеки) и далее либо собирает их все в отдельную папку, либо упаковывает в некий аналог самораспаковывающегося архива. Туда же добавляется код собственно интерпретатора, а также небольшая стартовая часть, которая при старте подготавливает среду исполнения и запускает интерпретацию целевого скрипта.
Поскольку ни создатели питона, ни создатели библиотек под него не задумывались о таком возможном использовании, всё это работает несколько «через пень-колоду». Основные проблемы заключаются в подготовке списка всех необходимых библиотек и модулей. Потому что многие авторы библиотек используют неявное импортирование, при котором нет явного указания на то, какой модуль будет импортирован в скрипт или другой модуль. Соответственно частенько после автоматического анализа скрипта pyInstaller-ом надо руками дописывать в настроечный файл (с расширением .spec) имена дополнительных модулей и/или путей, где их искать. Вместе с pyInstaller-ом идёт большой набор готовых функций для таких библиотек, но жизнь не стоит на месте. В частности так оказалось с текущей версией ZMQ — разработчики pyZMQ (биндинга ZMQ к python-у) изменили механизм импортирования вспомогательных библиотек, в результате чего собранное pyInstaller-ом приложение не запускается. Народ с этим делом уже разобрался и соответствующий патч для pyInstaller-а подготовили. Патч этот рабочий, но в официальный релиз пока не попал, поэтому пришлось руками патчить pyInstaller. Код патча смотри на
github.com/pyinstaller/pyinstaller/pull/110/files
Но даже этого патчка оказалось мало в случае, если standalone приложение должно представлять из себя только один исполнимый файл, а не целую папку. Пришлось руками ещё корректировать .spec-файл, чтобы библиотека libsodium.pyd из дистрибутива ZMQ попала в нужное место при распаковке .exe-файла.
Вторая проблема заключалась в модуле multiprocessing. Бэкенд я разбил на две части, которые должны работать параллельно. Мне показалось неправильным, если для запуска бэкенда придётся запускать две отдельные программы. А использовать многопоточность (multithreading) мне показалось неправильным из-за наличия GIL (Global Interpretation Lock). Казалось бы, чего проще написать простенький скрипт, который при старте будет просто порождать два новых процесса: один для ZMQ-сервера, читающего данные из COM-порта, второй — для HTTP-сервера, отвечающего на запросы веб-клиентов. И действительно, при работе в обычном режиме (т. е. при «ручном» запуске интерпретатора) прекрасно работает следующий скрипт:
# -*- coding: utf-8 -*-
from multiprocessing import Process
import serial_to_zmq
import zmq2web_using_flask
import time
def main():
args = get_command_line_params()
p1 = Process( target=serial_to_zmq.work, args=(args.serial_port_name, args.serial_port_speed, args.zmq_pub_addr) )
p1.start()
p2 = Process(target=zmq2web_using_flask.work, args=(args.zmq_sub_addr,))
p2.start()
print 'Press Ctrl+C to stop...',
while True:
time.sleep(10)
def get_command_line_params():
...
if __name__ == '__main__':
main()
Но после обработки pyInstaller-ом полученный .exe нормально не работал. Происходил запуск нескольких ZMQ-серверов и Flask-приложений с соответствующими сообщениями о том, что «сокет уже занят» и ещё какие-то странности. Выяснилось, что при запуске Flask-приложения ему надо передавать параметр debug=False:
app.run(host='0.0.0.0', debug=False, threaded=True)
а для модуля multiprocessing нужно вызывать специальную функцию freeze_support(), которая нужна в том режиме (froozen), который создаётся при интерпретации скрипта в случае standalone приложения, созданного pyInstaller-ом.
Вообщем, итог по данному пункту таков: создать standalone приложение из питоновского скрипта можно, но это не просто.
Все исходные тексты можно взять на гитхабе:
github.com/alguryanow/serial2web-2
P.S. Ещё учебник по Bootstrap:
www.tutorialrepublic.com/twitter-bootstrap-2.3.2-tutorial