Настільний пульт управління на 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 працює як машина станів (state-машина).
— Поточний стан компонента визначають дві групи значень: статичні властивості 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


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.