1i7 (1i7) wrote,
1i7
1i7

Category:

Настольный пульт управления на JavaScript/Node.js для робота на Ардуине

Настольный пульт управления на JavaScript/Node.js для робота на Ардуине

Сегодня делаем настольное приложение с графическим интерфейсом для управления роботом на Ардуине через последовательный порт. На языке JavaScript на платформе Electron с виджетами ReactJS+MaterialUI.

image



Теперь пульт управления для своего станочка с ЧПУ сделать не сложнее, чем написать сайтик.

Ранее:

— Часть 1: Консолька в роботе на Ардуине
— Часть 2: Управление роботом на Ардуино из приложения на Node.js



Главные ссылки

— Библиотека для робота: babbler_h
— Библиотека для Node.js: babbler-js
— Виджеты Babbler для Node.js+ReactJS+MaterialUI: babbler-js-material-ui
— Примеры приложений для babbler-js: babbler-js-demo

Ссылки на инструменты

— Последовательный порт в Node.js node-serialport: github.com/EmergingTechnologyAdvisors/node-serialport
— Платформа Electron: electron.atom.io
— ReactJS: facebook.github.io/react
— Виджеты (компоненты) MaterialUI для ReactJS: www.material-ui.com/#

Дополнительные ссылки

— Платформа NWJS: nwjs.io
— Другие виджеты для React:
github.com/facebook/react/wiki/Complementary-Tools#ui-components

Быстрый старт


1. Прошивайте в Ардуино скетч babbler_json_io.ino из предыдущей истории

Эта прошивка обменивается данными в формате JSON, умеет мигать лампочкой, содержит 4 команды: ping, help, ledon, ledoff

2. Качайте пульт управления:


git clone https://github.com/1i7/babbler-js-demo.git
cd babbler-js-demo/babbler-serial-react
npm install



3. Запускайте пульт управления:


./babbler-serial.sh



4. Нажимайте на кнопочки, мигайте лампочкой:

image

Структура проекта


Задача проекта — запустить библитеку Babbler.js для общения с роботом в окружении Electron (запускалка приложений JavaScript в отдельном окне, основана на коде Google Chrome), для графического интерфейса подключить ReactJS с виджетами MaterialUI. В общем, все не сложнее «Здравствуй мира» для перечисленных проектов, но в процессе собирания всех этих библиотек в одном приложении было выявлено несколько проблем и нюансов, поэтому в качестве шаблона новых проектов рекомендую брать за основу исходники этого примера.

Предварительные требования: иметь на компьютере установленными node.js, npm и (желательно) git.

Еще раз, качаем исходники и идем в проект babbler-js-demo/babbler-serial-react:


git clone https://github.com/1i7/babbler-js-demo.git
cd babbler-js-demo/babbler-serial-react



Файлы проекта

package.json — проект для npm (Node package manager): настройки проекта, список зависимостей.

Благодаря ему мы можем установить все зависимости, перечисленные в package.json (включая платформу Electron), одной командой внутри проекта:


npm install



main.js — главный файл для приложения Electron (взят из какого-то «здравствуй мир» для электрона).

Содержимое говорит само за себя. Единственное интересное место — команда, открывающая панель с инструментами разработки при старте приложения:


    // Открываем DevTools.
    mainWindow.webContents.openDevTools();



Рекомендую оставлять этот вызов при разработке приложения (открыть вручную через меню: View → Toggle Developer Tools) и удалять/комментировать при релизе.

index.html — содержимое главного экрана приложения Electron.

Т.к. для формирования графического интерфейса мы используем React, всё, что должно быть внутри body, — элемент div с id=«app-content»


<body>
    <div id="app-content"></div>
</body>



Этот файл мы править не будем, главный код будет дальше.

react-app-ui.js — главный файл приложения, здесь главное дерево виджетов для главного экрана (отправляется в index.html в div с id=«app-content»), весь пользовательский код, правим только его.

babbler-serial.sh — скрипт-запускалка приложения


#!/bin/sh
./node_modules/electron-prebuilt/dist/electron .



Дополнительные нюансы

Для заметки

Проблема с node-serialport и Electron

Библиотека node-serialport не захотела работать на последних версиях платформы Electron (при том, что на «голом» Node.js всё было прекрасно).

Не вдаваясь в подробности, отмечу, что проблему можно обойти, откатившись на старую версию Electron 1.1.3 или (как пишут в одном из обсуждений) пересобрать его из исходников.


sudo npm i -g electron-prebuilt@1.1.3



Эта же проблема наблюдается в платформе NWJS (альтернатива Electron), очевидно, что-то поломали в движке Хрома, на котором они все основаны.

Работающая версия Electron указана в зависимостях проекта в package.json, поэтому с демо-проектом всё ок.

Сообщения в баг-трекерах проектов:

Node-serialport
Electron
NWJS

Возможно, в одном из следующих релизов проблема будет исправлена, в таком случае можно будет переключить Electron на более свежую версию.

Babel, синтаксис JSX и ES6

Приложение и компоненты ReactJS используют специальный синтаксис JSX — это HTML-подобный XML для описания структуры дерева элементов управления приложения прямо в коде JavaScript. Так же внутри приложения мы будем местами использовать расширенный синтаксис JavaScript ES6 (это набор всевозможных синтаксических конструкций языка, которые еще не вошли в стандарт JavaScript или вошли в него не так давно, поэтому пока не реализованы даже в самых свежих версиях браузеров). Сначала я хотел исключить конструкции ES6 (их все можно заменить на аналоги из «классического» JavaScript), чтобы не городить лишних конфигураций в проекте. Но потом сдался, т.к. многие примеры в интернете для ReactJS (и, в особенности, для MaterialUI) написаны с использованием синтаксиса ES6, и, в таком случае, мне бы пришлось всех их конвертировать в старый синтаксис JavaScript.

Для того, чтобы использовать нестандартный синтаксис на старом движке JavaScript, используют специальный инструмент — Babel. Он умеет на лету конвертировать нестандартные конструкции в их аналоги на обычном JavaScript, если в проекте задать правильные настройки. Здесь начинаются костыли и огород. В шаблоне проекта все необходимые настройки уже заданы, поэтому в подробности разбирать не буду, перечислю основные пункты:

— package.json должен содержать блок с настройками Babel:


"babel": { "presets": ["es2015", "react", "stage-1"] }



— Аналогичные настройки нужно указать в файле .babelrc, если в проект импортируете виджеты из каталогов за пределами текущего каталога (например: babbler-js-meterial-ui/src/.babelrc).

— Чтобы включить конвертацию Babel в блоках script (type=«text/babel») в HTML-файлах (у нас — index.html), в этом же файле нужно импортировать скрипт browser.min.js (локальная копия в проекте: babbler-serial-react/script/browser.min.js, чтобы не зависеть от интернета).

— Чтобы включить конвертацию Babel в отдельных js-файлах, нужно загрузить модуль 'babel-register', а сами js-файлы загружать через require('./react-app-ui.js'); (см всё тот же index.html).

Может быть через какое-то время новации ES6 перекочуют в основную ветку JavaScript в варианте Гугл Хрома (а оттуда — в Электрон) и часть этих подпорок можно будет выкинуть за ненадобностью.

Для работы виджетов MaterialUI в index.html необходимо загрузить модуль 'react-tap-event-plugin' и выполнить injectTapEventPlugin()

Смотрим исходники


Весь полезный пользовательский код расположен в одном файле — babbler-js-demo/babbler-serial-react/react-app-ui.js

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

Предварительные приготовления

Базовые объекты Реакта:


var React = require('react');
var ReactDOM = require('react-dom');



Виджеты MaterialUI — кнопки, иконки, табы, панельки:


// виджеты MaterialUI
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

import Paper from 'material-ui/Paper';
import {Tabs, Tab} from 'material-ui/Tabs';
import Divider from 'material-ui/Divider';

import RaisedButton from 'material-ui/RaisedButton';

import FontIcon from 'material-ui/FontIcon';
import {red200, green200} from 'material-ui/styles/colors';

import Subheader from 'material-ui/Subheader';



Виджеты Babbler из проекта babbler-js-material-ui — взаимодействие с устройством:

— BabblerConnectionPanel — панель подключения: выбор устройств из выпадающего списка, кнопки подключиться/отключиться (в зависимости от статуса подключения)
— BabblerConnectionStatusIcon — иконка статуса подключения к устройству: отключены, подключаемся, подключены
— BabblerConnectionErrorSnackbar — всплывающая внизу экрана панелька, извещающая о разрыве соединения и других ошибках подключения
— BabblerDataFlow — полный лог в реальном времени: добавление команды в очередь, обмен данными с устройством и т.п.
— BabblerDebugPanel (пока определен не в библиотеке, а внутри тестового проекта) — панель отладки: отправка команд устройству вручную, кнопки help, ping, лог с BabblerDataFlow


// виджеты Babbler MaterialUI
import BabblerConnectionStatusIcon from 'babbler-js-material-ui/lib/BabblerConnectionStatusIcon';
import BabblerConnectionErrorSnackbar from 'babbler-js-material-ui/lib/BabblerConnectionErrorSnackbar';
import BabblerConnectionPanel from 'babbler-js-material-ui/lib/BabblerConnectionPanel';
import BabblerDataFlow from 'babbler-js-material-ui/lib/BabblerDataFlow';

import BabblerDebugPanel from './widgets/BabblerDebugPanel';




Babbler.js для связи с устройством:


// Babbler.js
import BabblerDevice from 'babbler-js';



Стиль для кнопочек:


const btnStyle = {
  margin: 12
};




Наконец, самая интересная часть — общение с роботом, панель управления лампочкой

Панель — обычный компонент React: кнопка «Включить лампочку», кнопка «Выключить лампочку», иконка статуса лампочки.

Про компоненты React следует знать:

— Компонент React работает как машина состояний (стейт-машина).
— Текущее состояние компонента определяют две группы значений: статические свойства this.props и динамические состояния this.state.
— Статические свойства this.props: передаются через параметры тега компонента при добавлении его на экран.
— Динамические состояния this.state: меняются в процессе выполнения приложения, устанавливаются в нужный момент при помощи this.setState.
— Изменения состояний через this.setState приводит к перерисовке компонента.
— Перерисовка компонента происходит в функции render, внешний вид зависит от значений this.props и this.state.
— Внешний вид компонента внутри render определяется через синтаксис React JSX.

В нашем случае:

— Объект BabblerDevice попадает в компонент через статический параметр this.props.babblerDevice.
— События babblerDevice меняют динамические состояния компонента (если подключены, делаем все кнопки активными, если не подключены — делаем неактивными).
— Кнопки «Включить лампочку» и «Выключить лампочку» отправляют команды ledon и ledoff устройству через babblerDevice.
— В случае получения положительного ответа «ok» меняют картинку статуса лампочки через запись значения свойства this.state.ledOn (true/false).


// Управление лампочкой
var BabblerLedControlPnl = React.createClass({
// http://www.material-ui.com/#/components/raised-button
// http://www.material-ui.com/#/components/subheader

    getInitialState: function() {
        return {
            deviceStatus: this.props.babblerDevice.deviceStatus(),
            ledOn: false
        };
    },
    
    componentDidMount: function() {
        // слушаем статус устройства
        this.deviceStatusListener = function(status) {
            this.setState({deviceStatus: status});
        }.bind(this);
        this.props.babblerDevice.on(BabblerDevice.Event.STATUS, this.deviceStatusListener);
    },
    
    componentWillUnmount: function() {
        // почистим слушателей
        this.props.babblerDevice.removeListener(BabblerDevice.Event.STATUS, this.deviceStatusListener);
    },
    
    render: function() {
        var connected = this.state.deviceStatus === BabblerDevice.Status.CONNECTED ? true : false;
        return (
            <div style={{textAlign: "center"}}>
                <div>
                    <RaisedButton label="Включить лампочку" onClick={this.cmdLedon} disabled={!connected} style={btnStyle} />
                    <RaisedButton label="Выключить лампочку" onClick={this.cmdLedoff} disabled={!connected} style={btnStyle} />
                </div>
                
                <FontIcon 
                    className="material-icons" 
                    style={{fontSize: 160, marginTop: 40}}
                    color={(this.state.ledOn ? green200 : red200)}
                >{(this.state.ledOn ? "sentiment_very_satisfied" : "sentiment_very_dissatisfied")}</FontIcon>
                     
            </div>
        );
    },
    
    cmdLedon: function() {
          this.props.babblerDevice.sendCmd("ledon", [],
              // onReply
              function(cmd, params, reply) {
                  if(reply == 'ok') {
                      this.setState({ledOn: true});
                  }
              }.bind(this),
              // onError
              function(cmd, params, err) {
                  console.log(cmd + (params.length > 0 ? " " + params : "") + ": " + err);
              }.bind(this)
          );
      }, 
      
      cmdLedoff: function() {
          this.props.babblerDevice.sendCmd("ledoff", [],
              // onReply
              function(cmd, params, reply) {
                  if(reply == 'ok') {
                      this.setState({ledOn: false});
                  }
              }.bind(this),
              // onError
              function(cmd, params, err) {
                  console.log(cmd + (params.length > 0 ? " " + params : "") + ": " + err);
              }.bind(this)
          );
      }
});




Главный экран приложения

Создаем устройство BabblerDevice для подключения к роботу:


// Устройство Babbler, подключенное к последовательному порту
var babblerDevice1 = new BabblerDevice();



Финальная верстка главного экрана приложения — синтаксис ReactJS JSX (HTML-подобный XML внутри кода JavaScript). Рисуем дерево элементов управления, отправляем в index.html в div с id='app-content'.

Здесь у нас панель подключения к устройству — полоска наверху, блоки общения с роботом — внутри табов.


// Контент приложения
ReactDOM.render(
    <MuiThemeProvider muiTheme={getMuiTheme()}>
      <div>
        <Paper>
            <BabblerConnectionPanel babblerDevice={babblerDevice1}/>
            <BabblerConnectionStatusIcon 
                babblerDevice={babblerDevice1} 
                iconSize={50}
                style={{position: "absolute", right: 0, marginRight: 14, marginTop: 5}} />
        </Paper>
        
        <Divider style={{marginTop: 20, marginBottom: 20}}/>
        
        <Tabs>
            <Tab label="Лампочки" >
                <BabblerLedControlPnl babblerDevice={babblerDevice1}/>
            </Tab>
            <Tab label="Отладка" >
                <BabblerDebugPanel babblerDevice={babblerDevice1}/>
            </Tab>
            <Tab label="Лог" >
                <BabblerDataFlow 
                    babblerDevice={babblerDevice1} 
                    reverseOrder={true}
                    maxItems={10000}
                    timestamp={true}
//                    filter={{ err: false, data: false }}
//                    filter={{ data: {queue: false} }}
//                    filter={{ err: {in: false, out: false, queue: false}, data: {in: false, out: false, queue: false} }}
                    style={{margin: 20}}/>
            </Tab>
        </Tabs>
        
        <BabblerConnectionErrorSnackbar babblerDevice={babblerDevice1}/>
      </div>
    </MuiThemeProvider>,
    document.getElementById('app-content')
);



Запускаем




./babbler-serial.sh



Выбираем устройство:

image

Подключаемся:

image

Ждем:

image

Включаем лампочку:

image

Выключаем лампочку:

image

Смотрим лог:

image

Шлем команды в ручном режиме:

image

image





подсветка синтаксиса
Tags: arduino, babbler, chipkit, роботы, типовые задачи, хабра
Subscribe

Posts from This Journal “babbler” Tag

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 1 comment