Подключение серво-машинок к роутеру. Часть 2

Во второй части предлагаю рассмотреть еще одну интересную вещь под названием — Luci.

Управление сервами из командной строки - это конеш круто, но, наверное, хотелось бы чего-то более графического. Поэтому предлагаю сделать управление нашей серво-сетью через веб-интерфейс.

Как известно, в OpenWrt за веб-интерфейс отвечает некая (почему-то так охота, чтоб это была «некая») Luci. Вообще luci - это очень хороший пример,, как красиво, грамотно и рационально можно использовать в одном проекте целую комбинацию языков — JavaScript, Ajax, Lua, Html, Css и Си. Тот кто написал Luci, наверно, очень крут )))

Что же нужно сделать нам? Писать Luci с нуля нам не надо ). Нам нужно всего лишь написать свой мааааленький модулёк для люси. Как это сделать, хорошо написано на официальном сайте Luci. Попробуем это повторить.

Создаем директорию luci-servo-manager в /trunk/build_dir/target-mipsel_uClibc-0.9.32/luci-0.10/applications.

Создаем в ней директорию luasrc. В luasrc создаем директории view и controller.

Немного комментариев. Если зайти в make menuconfig, то можно увидеть, что люси имеет модульную структуру. Каждый модуль добавляется в люси, как только мы поставим <*> напротив его. Если вы помните, мы уже собирали люси с модулем samba в одном из предыдущих разделов. Так вот, все модули имеют абсолютно одинаковую структуру, которую заложили авторы люси. Поэтому, чтобы добавить свой модуль, мы должны создать эту структуру, что и делаем сейчас.

В директории controller создаем файл servo-manager.lua следующего содержания:

module("luci.controller.servo-manager", package.seeall) 

function index() 
        entry({"admin", "ServoManager"}, template("servo-manager/sm_cfg"), "ServoManager", 50).dependent=false 
end

Этот файл имеет единственную функцию index, которая добавит наш модуль в структуру люси.
{"admin", "ServoManager"} — это путь в строке адреса до страницы, которую мы создаем.

template("servo-manager/sm_cfg") - означает, что для отображения нашей страницы используется файл sm_cfg.htm, который находится в поддиректории с именем «servo-manager».

"ServoManager" — это имя вкладки в веб интерфейсе

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

module("luci.controller.servo-manager", package.seeall) - создает наш модуль с именем servo-manager.

Теперь в директории /luci-0.10/applications/luci-servo-manager/luasrc/view/servo-manager нам нужно создать html страничку, на которой будет возможность управлять сервой.

Как управлять сервой из веб-интерфейса? Для этого предлагаю использовать элемент TrackBar — это бегунок с дискретным шагом. Элемента TrackBar нет в стандартном html, поэтому будем использовать виджет из библиотеки jquery. В jquery данный виджет называется slider.

Выглядеть это будет так:

А это сама страничка на html:

<%#
		страница servo-manager
-%>

<%
-- Код на lua. Здесь происходит обработка ajax запроса. Т.е. этот код
-- выполняется на сервере.


-- проверим, что параметр "status" равен 1
	if luci.http.formvalue("status") == "1" then

-- отладка. использовал её при запуске люси на хосте
--		io.write  ("получен ajax запрос","\n")

-- отрабатываем изменение положения
-- отладка. использовал её при запуске люси на хосте
--  print (luci.http.formvalue("serv_num"))
--	print(luci.http.formvalue("state"))

-- сформируем командную строку
	 str = "lua /home/WebServoManager.lua".." "..(luci.http.formvalue("serv_num")).." "..(luci.http.formvalue("state"))

-- выполним команду
	 os.execute (str)
	 
--	отладка	 
--		io.output (io.open ("/home/temp.txt", "w"))
--		io.output (io.open(("/home/temp.txt","a"))
--    io.write  (str)
--    io.close()	 

		local temp_value = 100;

		local rv = {
			temp_value = temp_value,
		}

-- сформируем данные для отправки клиенту и отправим
		luci.http.prepare_content("application/json")
		luci.http.write_json(rv)

		return
	end

--	local system, model = luci.sys.sysinfo()
-%>

<%+header%>
<fieldset class="cbi-section">

<div class="cbi-map">
	<link rel="stylesheet" href="<%=resource%>/jslider.css" type="text/css" />

	<script type="text/javascript" src="<%=resource%>/jquery-1.4.2.js"></script>
	<script type="text/javascript" src="<%=resource%>/jquery.dependClass.js"></script>
	<script type="text/javascript" src="<%=resource%>/jquery.slider-min.js"></script>

	<style type="text/css" media="screen">
		td {border:2px solid LightSlateGray;text-align:center;}
		th {border:2px solid LightSlateGray;cellspacing:0px;text-align:center;}
		table {table-layout:auto; width:1000px; border:2px solid LightSlateGray;border-collapse:collapse}
		.layout-slider { margin-top:20px;margin-bottom:20px; width: 100%;}
	</style>

	<fieldset class="cbi-section">
		<legend><%:Servo-Manager%></legend>

		<table>
			<tr>
				<th style="width:50px"><%:Номер сервы%></th>
				<th style="width:120px"><%:Рисунок сервы%></th>
				<th><%:Положение Сервы%></th>
			</tr>
			<tr>
				<td>
						<span style="background-color:#FFFFFF; border:1px solid #CCCCCC; padding:2px">1</span>
				</td>
				<td>
					<div><img src="<%=resource%>/servo1.jpg" width="100%"></img></div>
				</td>
				<td>
					<div>
						<div class="layout-slider">
							<input id="SliderSingle" type="slider" name="rotate" value="20" />
						</div>

						<script type="text/javascript" charset="utf-8">
							// создаем объект jQuery - слайдер
							jQuery("#SliderSingle").slider({
								from: 0,
								to: 180,
								step: 1,
								round: 0,
								dimension: ' grad',
								skin: "round",
								callback: CallOnRelease,
							});

							
							var iwxhr = new XHR();
							/* ajax запрос к серверу. Функция SetNewState будет вызвана
							при изменении положения слайдера. В этой функции будет осуществлен
							ajax запрос к серверу. Параметры для сервера будут переданы следующие:
							status:1,serv_num:serv_num,state:new_state */
							function SetNewState(serv_num,new_state) {
								iwxhr.get('<%=REQUEST_URI%>', { status:1,serv_num:serv_num,state:new_state },
									function(x, info)	{
									}
								)
							}
							// обработка события - "изменение положения слайдера"
							function CallOnRelease() {
								SetNewState(1,jQuery("#SliderSingle").slider('value'))
							// отладка в консоль firebug
								console.log ("Change_handle=",jQuery("#SliderSingle").slider('value'))
							}

							// состояние по умолчанию
						  jQuery("#SliderSingle").slider('value',90)
							
							// отладка в консоль firebug
							console.log("slider-state=",jQuery("#SliderSingle").slider('value'))

						</script>
					</div>
				</td>
				</tr>
		</table>
	</fieldset>
</div>
</fieldset>

<%+footer%>

На первый взгляд код странички очень сложен. И с этим можно согласиться, потому что здесь используется lua, html, css, javascript, ajax. С другой стороны, всё очень просто.

Между <%+header%> и <%+footer%> формируется сама страничка. Создается табличка из трех столбцов. В третьем столбце помещаем слайдер, который реализован на javascript. Там же описана функция SetNewState, которая будет вызываться всякий раз, когда слайдер изменит своё значение. В этой функции происходит ajax обращение к серверу с помощью библиотеки xhr.js. При ajax запросе, происходит передача параметров: передается флаг статус = 1, номер сервы и её новое положение.

Код на луа, который располагается вверху файла, как раз занимается обработкой ajax запроса и выполняется на сервере. Проверяется значение параметра status, и если оно равно 1, то происходит вызов скрипта WebServoManager.lua с параметрами.

Скрипт WebServoManager.lua - это скрипт ServoManager.lua, но немного изменен. Вот так он выгладит:

--[[ Подключаем библиотеку для работы с com портом. Можно было бы
			написать просто require("luars232"), то тогда обращение бы шло
			по имени модуля luars232.RS232_BAUD_115200 ]]
rs232 = require("luars232")

local SET_NEW_STATE					=		10
local SET_NEW_STATE_ANSWER	=		11

-- Определяем имя порта
port_name = "/dev/ttyUSB0"

-- определим канал для вывода сообщений
local out = io.stderr

-- определим канал для ввода данных с клавиатуры
local user_in = io.stdin

--[[ Вызываем функцию open для открытия порта.Эта функция возвращает 
		 два значения, которые присваиваются переменным e и p
		 e - тип ошибки, p - дескриптор порта ]]
local e, p = rs232.open(port_name)

-- если ошибка открытия
if e ~= rs232.RS232_ERR_NOERROR then
	-- handle error
	out:write(string.format("can't open serial port '%s', error: '%s'\n",
			port_name, rs232.error_tostring(e)))
	return
end

-- Настраиваем порт, вызывая соотвествующие методы. Методы описаны в luars232.c
assert(p:set_baud_rate(rs232.RS232_BAUD_115200)	== rs232.RS232_ERR_NOERROR)
assert(p:set_data_bits(rs232.RS232_DATA_8)			== rs232.RS232_ERR_NOERROR)
assert(p:set_parity(rs232.RS232_PARITY_NONE)		== rs232.RS232_ERR_NOERROR)
assert(p:set_stop_bits(rs232.RS232_STOP_1)			== rs232.RS232_ERR_NOERROR)
assert(p:set_flow_control(rs232.RS232_FLOW_OFF)	== rs232.RS232_ERR_NOERROR)

out:write(string.format("OK, port open with values '%s'\n", tostring(p)))

--[[ Порт настроен. Можно осуществлять обмен данными. Алгоритм управления 
   сервой следующий: 1. Мастер посылает пакет, в котором указывается номер
   сервы и то состояние, в которое она должна перейти. 2. Серва отвечает
   мастеру, что пакет получен и команда исполнена
   Протокол общения:                                      
		Запрос:                                                
		|Длина|Кому|№команды|Тело|Кс|
		Ответ:                                                 
		|Длина|ОтКого|№команды+1|Тело|Кс| ]]

--[[ при вызове скрипта были переданы параметры.
	Параметр 1 - номер сервы. Параметр 2 - положение сервы. ]]

	input_args = {}

	input_args[1] = arg [1]
	input_args[2] = arg [2]

	--[[ сформируем буфер для отправки
		Запрос:                                                
		|Длина|Кому|№команды|Тело|Кс| ]]
		
	local tx_buf={5}
	table.insert (tx_buf,input_args[1])
	table.insert (tx_buf,SET_NEW_STATE)
	table.insert (tx_buf,input_args[2])
	table.insert (tx_buf,(tx_buf[1]+tx_buf[2]+tx_buf[3]+tx_buf[4])%0xff)

	-- Посылаем пакет
	err, len_written = p:write(tx_buf)
	assert(e == rs232.RS232_ERR_NOERROR)
	
	-- выведем на экран то, что послали
	out:write (string.format(">Передан пакет %d байт:",len_written))
	for i=1,len_written,1 do
		out:write(string.format(" %d",tx_buf[i])) 
	end
	out:write(string.format("\n"))	

	-- Ждем ответа

	-- предположим, что максимальный пакет не может быть больше 100 байт
	local read_len = 100 

	-- ждать ответа будем 100мс
	local timeout = 100

	-- запускаем функцию чтения данных из порта
	local err, data_read, size = p:read(read_len, timeout,1)
	assert(e == rs232.RS232_ERR_NOERROR)

	-- выведем на экран то, что получили через уарт
	out:write(string.format("<Принят пакет %d байт:",size))
	for i=1,size,1 do
		out:write(string.format(" %d",string.byte(data_read,i))) 
	end
	out:write(string.format("\n"))

-- close
assert(p:close() == rs232.RS232_ERR_NOERROR)

<%+header%> и <%+footer%> - это файлы header.htm и footer.htm, которые отвечают за начало и конец html файла. Допустим, в файл header.htm можно было бы добавить рисунок для шапки сайта. А в файл footer.htm рисунок, который бы располагался внизу на всех страницах сайта.

Библиотеки javascript подключаются стандартным способом. При этом <%=resource%> - это вот такой путь luci-0.10/applications/luci-servo-manager/htdocs/luci-static/resources. Поэтому нужно создать эту папку и поместить туда все файлы, которые используются.

Ну и последнее. Нужно добавить наш модуль в состав openwrt. Для этого в файл /trunk/feeds/luci/luci/Makefile нужно добавить вот такую строчку:

$(eval $(call application,servo-manager,servo-manager - control servo from web))

Всё. Заходим в make menuconfig и отмечаем наш модуль.

LuCI :
Applications:
<M>luci-app-servo-manager - servo-manager - control servo from web

Устанавливем:

root@OpenWrt:/# opkg install luci-app-servo-manager 
Installing luci-app-servo-manager (0.10+svn6982-1) to root... 
Downloading ftp://ftp:ftp@192.168.0.9/luci-app-servo-manager_0.10+svn6982-1_brcm47xx.ipk. 
Configuring luci-app-servo-manager. 

Открываем веб-интерфейс и проверяем.

Все необходимые файлы можно взять в разделе «скачать».



Купить адаптер для серво-машинки у нас:

Адаптер для серво-машинки