суббота, 21 марта 2015 г.

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

Подключение серво-машинок к роутеру. Часть 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. 
Открываем веб-интерфейс и проверяем.
Все необходимые файлы можно взять в разделе «скачать».


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

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

1 комментарий:

  1. Александр, копаюсь в Luci, хочу сделать свое меню.
    Но у меня при вызове make menuconfig не появляется возможность выбора в меню luci-app-servo-manager. Подскажите с чем это связано, может предварительно требуется подача какой-либо команды?

    ОтветитьУдалить