Предисловие
В данной статье я хотел бы поделиться своим опытом работы с сенсорным дисплеем и ардуино уно с аппаратной стороны и с Node.js с программной стороны компьютера.
Однажды мне стало интересно, как же рисуют профессиональные дизайнеры и художники в 21 веке, и я обнаружил, что сегодня цифровые изображения и рисунки создают с помощью цифрового планшета, да не простого, а специального, с чувствительностью к нажатиям и углом стилуса. Так появилась идея создать собственный «дигитайзер». Так как я веб разработчик, то соответственно на стороне ПК я решил использовать веб технологии, но с таким же успехов можно использовать все что угодно. Я расскажу и покажу как передавать на арудуино через serial port данные в простом и удобном формате JSON.
Аппаратная часть
Нам потребуется:
- Arduino UNO
- Touch TFT LED display 320 x 480 ( или меньшее, потому что максимальное разрешение для 32 кбайт памяти Ардуинки)
Экран с 9.7 см x 6.9 см , 3.95 дюйма был заказан на aliexpress. Обрабатывает также силу нажатия, что мы потом будем использовать для отрисовки. Занимает все порты, так что если нужны будут, то придется параллелить и так красиво на ардуино не «встанет» . Экран заточен под арудино, поэтому он легко становится, из-за высокого разрешения, он занимает все пины.
Необходимые библиотеки для работы с дисплеем и тачем:
- Adafruit-GFX-Library
- MCUFRIEND_kbv
- TFTLCD
- Touch-Screen-Library
Находятся они быстро и легко на Github. После скачивания, папки с библиотеками нужно поместить в Arduino/ibraries.Вообще хорошей документации я так и не нашел, поэтому пришлось тянуть по кусочку с различных примеров и смотреть исходники. В конце статьи будет ссылка на исходники, где также можно найти эти библиотеки.
Основная идея состоит в том, что в левом углу мы отрисовываем 6 квадратов цветов, при нажатии на которые выбирается соответственный цвет. При касании «рабочей» области рисуем круг с радиусом в зависимости от силы нажатия. При очень сильном нажатии — чистим экран. При каждом касании экрана формируем json и отправляем его на ПК.
Собственно код основной функции с комментариями(весь код прикреплен файлом и его можно найти по ссылке в репозитории):
void loop()
{
uint16_t xpos, ypos; //координаты экрана
tp = ts.getPoint(); //tp.x, tp.y являются значения АЦП
// мы имеем некоторое минимальное давление, которое считаем «действительным»
// если 0 то нет касания
if (tp.z > MINPRESSURE && tp.z < MAXPRESSURE) {
// если не портретный режим
if (SwapXY != (Orientation & 1)) SWAP(tp.x, tp.y);
//здесь нужно было вывести свои мат функции зависимости, т.к. считывается значения АЦП
//а графическая библиотека рисует по координатам соответствующим от 0 до 320 по оси x
// и 0 до 480 по оси Y
xpos = -80*(tp.x -516) /23 + 320;
ypos = -5*(tp.y -187)/7 +480;
ypos-= 55;
// если мы «зашли» на полоску цветов
if (ypos < BOXSIZE) { //нарисовать белую рамку на выбранном цвете
oldcolor = currentcolor;
if (xpos < BOXSIZE) {
currentcolor = RED; // запоминаем для дальнейшей отрисовки новый цвет
colorForSerial = 0;// запоминаем код для передачи на пк
tft.drawRect(0, 0, BOXSIZE, BOXSIZE, WHITE);
} else if (xpos < BOXSIZE * 2) {
currentcolor = YELLOW;
colorForSerial = 1;
tft.drawRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, WHITE);
} else if (xpos < BOXSIZE * 3) {
currentcolor = GREEN;
colorForSerial = 2;
tft.drawRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, WHITE);
} else if (xpos < BOXSIZE * 4) {
currentcolor = CYAN;
colorForSerial = 3;
tft.drawRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, WHITE);
} else if (xpos < BOXSIZE * 5) {
currentcolor = BLUE;
colorForSerial = 4;
tft.drawRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, WHITE);
} else if (xpos < BOXSIZE * 6) {
currentcolor = MAGENTA;
colorForSerial = 5;
tft.drawRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, WHITE);
}
if (oldcolor != currentcolor) { убираем предыдущую белую рамку
if (oldcolor == RED){
tft.fillRect(0, 0, BOXSIZE, BOXSIZE, RED);
return;
}
if (oldcolor == YELLOW){
tft.fillRect(BOXSIZE, 0, BOXSIZE, BOXSIZE, YELLOW);
return;
}
if (oldcolor == GREEN){
tft.fillRect(BOXSIZE * 2, 0, BOXSIZE, BOXSIZE, GREEN);
return;
}
if (oldcolor == CYAN){
tft.fillRect(BOXSIZE * 3, 0, BOXSIZE, BOXSIZE, CYAN);
return;
}
if (oldcolor == BLUE){
tft.fillRect(BOXSIZE * 4, 0, BOXSIZE, BOXSIZE, BLUE);
return;
}
if (oldcolor == MAGENTA){
tft.fillRect(BOXSIZE * 5, 0, BOXSIZE, BOXSIZE, MAGENTA);
return;
}
}
}
if (((ypos — PENRADIUS) > BOXSIZE) && ((ypos + PENRADIUS) )) {
tft.fillCircle(xpos, ypos, getPressure(tp.z), currentcolor); // каждое нажатие рисует круг, чем сильнее нажатие, тем больше радиус
String jsonString = «{«x»:»»; // формируем json строку вида {x:1,:y:2,z:3,color:2}
jsonString += xpos;
jsonString +=»»,»y»:»»;
jsonString += ypos;
jsonString +=»»,»z»:»»;
jsonString += getPressure(tp.z);
jsonString += «»,»c»:»»;
jsonString += colorForSerial;
jsonString +=»»}n»;
Serial.print(jsonString); // отправляем по serial port’у
}
// при очень сильном нажатии очищаем экран
if (tp.z <= 25) {
tft.fillRect(0, BOXSIZE, tft.width(), tft.height() — BOXSIZE, BLACK);
}
}
}
Собственно с аппаратной частью покончено. Как оно выглядит
ПК часть
На ПК будет слушать Node.js сервер, который по вебсокету будет передавать наш json в браузере, где будет собственно происходить рендеринг картинки.
На фронтэнд части используется React и библиотека Paper.js. В качестве сборщика используется Webpack. Но обо всем по порядку.
Серверная часть (node.js):
Необходимые зависимости хранятся в файлике package.json
package.json
{
«name»: «api»,
«version»: «1.0.0»,
«description»: «»,
«main»: «server.js»,
«scripts»: {
«test»: «echo «Error: no test specified» && exit 1″,
«start»: «node server.js»
},
«author»: «»,
«license»: «ISC»,
«dependencies»: {
«body-parser»: «^1.13.3»,
«cookie-parser»: «^1.3.5»,
«cookie-session»: «^1.2.0»,
«express»: «^4.13.3»,
«passport»: «^0.3.0»,
«passport-local»: «^1.0.0»,
«sails-mysql»: «^0.11.0»,
«serialport»: «^3.1.2-beta1»,
«socket.io»: «^1.4.6»,
«waterline»: «^0.10.26»
}
}
Для установки зависимостей нужно ввести npm i.
Для транспортировки данных будет использоваться прекрасная библиотека Socket.io https://www.npmjs.com/package/socket.io , которая создает вебсокет, благодаря которому можно транспортировать данные в реальном времени, не используя модель запрос-ответ.
Для чтения из порта будет использоваться serialport https://www.npmjs.com/package/serialport
Остальные библиотеки больше относятся к экосистеме и фреймворку express https://www.npmjs.com/package/express , которые используются для удобства, без которых вполне можно обойтись.
Основная логика серверного скрипта server.js
var socket = {};
var serialports = [];
var listenSerialport = null;
var sp = require(‘serialport’);
var router = express.Router();
router.post(‘/api/serialport’,function(req,res){ // запрос на выбор последовательного порта для чтения
if(listenSerialport == null) { // проверка открыли ли уже порт для чтения, чтобы заново не открывать
var nameOfPort = req.body.name;
serialports.forEach((item)=> { //ищем в в массиве запрошенный порт
if(item === nameOfPort){
listenSerialport = new sp.SerialPort(nameOfPort,{baudRate:19200,parser: sp.parsers.readline(‘n’)}); // создаем
listenSerialport.on(‘open’, function () {
console.log(«OPEN port» + nameOfPort);
});
listenSerialport.on(‘data’, function (data) { // при приходе данных с порта
try{
var serialData = JSON.parse(«» +data);
socket.emit(‘data’,serialData); // посылаем их клиенту в браузер
} catch (SyntaxError){ //обязательно ловим здесь исключения, потому
console.log(«Error»);//что бывают неприятные артефакты, когда приходит не вся json строка , а лишь часть
}
});
}
});
res.send({status:»OK»});
} else if(!res.finished)
res.send({status:»ERROR»});
});
sp.list(function(err, ports) { // получаем список последовательных портов на компьютере
ports.forEach((item)=>{
serialports.push(item.comName);
});
console.log(serialports);
});
В принципе все достаточно просто и очевидно. Никакой особенной логики тут нету. Запускается командой node server.js. (До этого необходимо собрать клиентскую часть)
Клиентская часть (React,Webpack,Paper.js):
Начнем с зависимостей:
package.json:
{
«name»: «paintduino»,
«version»: «1.0.0»,
«description»: «»,
«main»: «index.js»,
«scripts»: {
«test»: «echo «Error: no test specified» && exit 1″,
«dev»: «webpack-dev-server —port 5000 —content-base build —devtool eval —progress —colors —hot»,
«deploy»: «rm -rf ./dist && webpack -p —config webpack.production.config.js»
},
«author»: «»,
«license»: «ISC»,
«dependencies»: {
«babel-core»: «^5.8.22»,
«babel-loader»: «^5.3.2»,
«classnames»: «^2.2.3»,
«css-loader»: «^0.16.0»,
«extract-text-webpack-plugin»: «^0.8.2»,
«file-loader»: «^0.8.4»,
«html-webpack-plugin»: «^1.6.1»,
«image-webpack-loader»: «^1.6.1»,
«imagemin»: «^3.2.0»,
«jquery»: «^2.1.4»,
«json-loader»: «^0.5.4»,
«lodash»: «^3.10.1»,
«mobx»: «^2.1.3»,
«mobx-react»: «^3.0.4»,
«mobx-react-devtools»: «^4.0.2»,
«moment»: «^2.10.6»,
«paper»: «^0.9.25»,
«react»: «0.13.3»,
«react-addons-pure-render-mixin»: «^15.0.1»,
«react-bootstrap»: «^0.24.5»,
«react-chosen»: «^0.3.8»,
«react-dom»: «^15.0.1»,
«react-router»: «^0.13.3»,
«react-select»: «0.6.12»,
«react-slider»: «^0.5.1»,
«socket.io-client»: «^1.4.6»,
«style-loader»: «^0.12.3»,
«stylus-loader»: «^1.2.1»,
«webpack»: «^1.11.0»,
«webpack-dev-server»: «^1.10.1»
}
}
Я решил использовать библиотеку React.js https://facebook.github.io/react/ , на которой построен Facebook из-за гибкости и многих готовых компонентиков, из которых легко собрать свой интерфейс. В данном проекте React далеко не обязателен, можно было бы и обычным скриптом все сделать, но у меня была заготовка, которую я решил использовать. Реакт нужен для начальной настройки и каркаса приложения, всю логика отрисовки ложится на библиотеку Paper.js http://paperjs.org/
Отдельно хочу рассказать про сборщик для фронтэнда webpack https://webpack.github.io/ . Суть его в том, что к нему подключаются различные загрузчики контента (css, images, files, json, js) которые загружают картинки, таким образом можно разбить логику на странички. Допустим страница главная home, которая состоит из картинок, стилей и нашего скрипта , который генерирует из всего этого один большой скрипт. если глянуть на приведенный выше package.json, то можно увидеть в разделе scripts 3 команды: test, dev, deploy. Test не интересная, а вот deploy собирает все странички, все скрипты и картинки в один большой скрипт, html, стили и картинки в одно место, которое потом отдает сервер любому подключенному клиенту(браузеру). Команда dev используется для разработки очень удобная: webpack создает фактически сервер, который смотрит и анализирует на изменение кода, автоматические собирает все и обновляет страницу браузера, таким образом не нужно каждый раз обновлять страницу и собирать проект.
webpack.config.js:
var path = require(‘path’);
var HtmlWebpackPlugin = require(‘html-webpack-plugin’);
var webpack = require(«webpack»);
module.exports = {
context: __dirname + «/src»,
entry: [‘webpack/hot/dev-server’, path.resolve(__dirname, ‘src/main’)],
resolve: {
root: path.resolve(__dirname, ‘src’),
extensions: [», ‘.js’, ‘.jsx’, ‘.styl’],
},
output: {
path: path.resolve(__dirname, ‘build’),
publicPath : ‘http://localhost:5000/’,
filename: ‘bundle.js’,
},
module: {
loaders: [
{
test: /.jsx?$/, // A regexp to test the require path. accepts either js or jsx
loader: ‘babel’ // The module to load. «babel» is short for «babel-loader»
},
{
test: /.css$/, // Only .css files
loader: ‘style!css’ // Run both loaders
},
{
test: /.styl$/,
loader: ‘style-loader!css-loader!stylus-loader’
},
{
test: /.woff(d+)?$/,
loader: ‘url-loader?mimetype=application/font-woff’
},
{
test: /.(jpe?g|png|gif|svg)$/i,
loaders: [
‘file?hash=sha512&digest=hex&name=[hash].[ext]’,
‘image-webpack?bypassOnDebug&optimizationLevel=7&interlaced=false’
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: ‘./src/index.html’,
production: false,
inject: false,
}),
new webpack.ProvidePlugin({
$: «jquery»,
jQuery: «jquery»,
«window.jQuery»: «jquery»
}),
new webpack.ContextReplacementPlugin(/moment[/]locale$/, /de|fr|hu/)
],
devServer: {
proxy: {
‘*’: ‘http://localhost:7000’ //запросы проксируются на наш сервер
}
}
};
Также забыл упомянуть, что использую Babel для фишек ES6/7, которые пока что не поддерживают браузеры. Этот конфиг нужен для разработки.
Все клиентские скрипты описывать не буду, потому что они выходят за рамки проекта, остановлюсь лишь на основном.
dashboard.jsx
var React = require(‘react’); // подключаем необходимые модули-библиотеки
var io = require(‘socket.io-client’);
var http = require(‘../../utils/http.jsx’);
var {paper} = require(‘paper’); // библиотека, которая рисует
// Функии внутри компонента
getInitialState(){ //состояние компонента, порты и тот, который вабран
return {
ports:[],
selected:{}
}
},
componentDidMount(){ // когда компонент создан
var canvas = document.getElementById(‘myCanvas’);
paper.setup(canvas); // инициализируем графическую библиотеку
paper.view.draw();
var socket = io.connect(‘http://localhost:7000’); // открываем сокет
socket.on(‘serialports’, (data) => {
console.log(data);
this.setState({ports:data}); // получаем порты от сервера
});
socket.on(‘data’, function (data) { //когда приходят данные
console.log(data);
var z = getRadius(data.z);
if(z != 0){ // если не надо чистить экран
var x = data.x > 400 ? 0: data.x;
var circle = new paper.Path.Circle(new paper.Point(4.668*data.y -303.4,-2.43*x +850),z); //создаем окружности
circle.fillColor= getColor(data.c);
CirclesMas.push(circle); //собираем точки в массив
if(CirclesMas.length >= 10){ // т.к. данные слишком быстро приходят, браузер не успевает отрисовывать, поэтому когда достигли
paper.view.draw(); // «критической» массы — отрисовывам их всех сразу
CirclesMas = [];
}
} else{
paper.project.clear(); //чистим иначе
paper.view.draw();
}
});
},
onSend(){
http.post(‘/api/serialport’,{name:this.state.selected}).then((res)=>this.setState({selected:null})); // запрос на выбор порта
},
Для человека незнакомого с React’ом выглядит достаточно запутанно, но основные моменты, на которых строится вся логика закомментированы. Открываем вебсокет -> получаем порты -> выбираем порт -> получаем данные -> отрисовываем.
Команды для запуска:
$ cd frontend
$ npm run deploy
$ cd api
$ node server.js
Для разработки на фронтенде:
$ npm run dev
Ссылка на исходники : https://github.com/NikitaHurynovich/Paintduino
Демонстрация работы
+
7
Прикрепленные файлы:
- paintduino.ino (12 Кб)