Зачем нужно соединять Java-программу на компьютере и Arduino?
Картинка rawpixel
Любой энтузиаст, строящий свои проекты на базе Arduino, рано или поздно сталкивается с необходимостью тем или иным способом обеспечить взаимосвязь этой платы со своим компьютером.
Такой симбиоз даёт возможность как управлять платой с компьютера, так и наоборот — компьютером с платы. Об этом мы и поговорим в этой статье.
Постановка задачи и общие сведения о библиотеке
В рамках этой задачи я не имею в виду стандартное общение самой платы со средой разработки, я имею в виду, что условно необходимо реализовать «прямое» общение платы и компьютера. Конечно, это подразумевает собой наличие на компьютере некой утилиты, которая и будет осуществлять подобное общение.
Подобная программа может быть написана на любом известном вам языке программирования, но так уж исторически сложилось, что я более-менее понимаю java, поэтому разговор далее будет идти только об этом языке и библиотеках под него.
Некоторое время назад для решения подобного вопроса — использовалась библиотека RxTx, однако в её работе отмечаются некоторые проблемы, ввиду необходимости установки DLL под Windows, а также проблемы со стабильностью (судя по ряду мнений активных пользователей этой библиотеки. Возможно, у вас был другой опыт).
Сегодня есть гораздо более простая и удобная в использовании библиотека, которая позволяет организовать общение через COM-порты: jSerialComm.
Она удобна тем, что обеспечивает доступ комфортным, независимым от платформы способом, без использования каких-либо дополнительных инструментов и библиотек.
Подробное описание API этой библиотеки находится по этому адресу:
Так как сама библиотека не содержит достаточно большого количества примеров использования (только самые основные), то наверняка вам пригодится и вот эта подборка, в которой собрано 20 достаточно полезных коротких примеров использования возможностей библиотеки.
Такой, например, как инициализация порта:
public Serial(SerialPort port) throws IOException {
port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, TIME_OUT, TIME_OUT);
port.setComPortParameters(DATA_RATE, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY);
port.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED);
if (port.openPort()) {
this.port = port;
output = port.getOutputStream();
input = port.getInputStream();
} else {
throw new IOException("Cannot open serial port.");
}
}
Открытие порта:
@Override
public boolean openPort() throws Exception {
responseMessageHandler = new ResponseMessageHandler();
if (serialPort == null) {
throw new ConnectionException("The connection wasn't initialized");
}
return serialPort.openPort();
}
Закрытие порта:
@Override
public void closePort() throws Exception {
if (serialPort != null) {
serialPort.removeDataListener();
serialPort.closePort();
}
}
Но до начала всех манипуляций, нам необходимо вывести в консоль, и узнать, какие COM-порты у нас вообще доступны:
package main;
import com.fazecast.jSerialComm.SerialPort;
public class Starter {
public static void main(String[] args) {
System.out.println("Hello world");
SerialPort[] ports = SerialPort.getCommPorts();
for (SerialPort port: ports) {
System.out.println(port.getSystemPortName());
}
}
}
Библиотека может работать в ряде режимов, которые отличаются степенью блокировки (т.е., ожидания поступления данных).
Например, режимом по умолчанию является неблокирующий: методы, читающие данные, возвратят любые данные, которые доступны. Без какого-либо ожидания:
SerialPort comPort = SerialPort.getCommPorts()[0];
comPort.openPort();
try {
while (true)
{
while (comPort.bytesAvailable() == 0)
Thread.sleep(20);
byte[] readBuffer = new byte[comPort.bytesAvailable()];
int numRead = comPort.readBytes(readBuffer, readBuffer.length);
System.out.println("Read " + numRead + " bytes.");
}
} catch (Exception e) { e.printStackTrace(); }
comPort.closePort();
В противовес ему бывают ситуации, когда вы заранее знаете количество данных, которые должны получить. В такой ситуации вам нужно, чтобы метод, запрашивающий данные, дождался их получения и не возвращался «хоть с каким-нибудь» значением раньше времени.
Как раз для работы в таком режиме и предназначен полностью блокирующий способ. Например, в коде ниже, метод readBytes () ждёт запрошенные 1024 байта в течение 1 секунды:
SerialPort comPort = SerialPort.getCommPorts()[0];
comPort.openPort();
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 1000, 0);
try {
while (true)
{
byte[] readBuffer = new byte[1024];
int numRead = comPort.readBytes(readBuffer, readBuffer.length);
System.out.println("Read " + numRead + " bytes.");
}
} catch (Exception e) { e.printStackTrace(); }
comPort.closePort();
Если нужно, чтобы он бесконечно ждал поступления этих данных, то тогда внесите вот такое изменение в строке, где содержится setComPortTimeouts:
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
Более подробно обо всех доступных режимах вы можете прочитать здесь.
Также существует полностью асинхронный режим работы, в котором возможно прослушивание ряда событий и реагирование на них. В этом режиме тайм-ауты (всевозможные блокировки) игнорируются:
SerialPort comPort = SerialPort.getCommPorts()[0];
comPort.openPort();
comPort.addDataListener(new SerialPortDataListener() {
@Override
public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; }
@Override
public void serialEvent(SerialPortEvent event)
{
if (event.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE)
return;
byte[] newData = new byte[comPort.bytesAvailable()];
int numRead = comPort.readBytes(newData, newData.length);
System.out.println("Read " + numRead + " bytes.");
}
});
Например, в коде выше, при наличии любых данных, доступных для чтения — будет запущен обратный вызов.
Также (при желании) вы можете использовать стандартные Java интерфейсы Inputstream, Outputstream, например, так:
SerialPort comPort = SerialPort.getCommPorts()[0];
comPort.openPort();
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 0, 0);
InputStream in = comPort.getInputStream();
try
{
for (int j = 0; j < 1000; ++j)
System.out.print((char)in.read());
in.close();
} catch (Exception e) { e.printStackTrace(); }
comPort.closePort();
Подключаем Arduino к компьютеру
Случай, когда компьютер шлёт данные на Arduino
Теперь вернёмся к нашей задаче общения компьютера с Arduino. Понятно, что задача может быть двоякой: скажем так, «нисходящей» и «восходящей».
То есть, когда инициатором является компьютер или Arduino.
Хороший пример первого варианта можно посмотреть здесь. В нём содержится код как со стороны компьютера, так и со стороны Arduino. Рассмотрим компьютерную сторону:
package de.mschoeffler.arduino.serialcomm.example01;
import java.io.IOException;
import com.fazecast.jSerialComm.SerialPort;
/**
* Simple application that is part of an tutorial.
* The tutorial shows how to establish a serial connection between
* a Java and Arduino program.
* @author Michael Schoeffler (www.mschoeffler.de)
*
*/
public class Startup {
public static void main(String[] args) throws IOException, InterruptedException {
// device name TODO: must be changed
SerialPort sp = SerialPort.getCommPort("/dev/ttyACM1");
// default connection settings for Arduino
sp.setComPortParameters(9600, 8, 1, 0);
// block until bytes can be written
sp.setComPortTimeouts(SerialPort.TIMEOUT_WRITE_BLOCKING, 0, 0);
if (sp.openPort()) {
System.out.println("Port is open :)");
} else {
System.out.println("Failed to open port :(");
return;
}
for (Integer i = 0; i < 5; ++i) {
sp.getOutputStream().write(i.byteValue());
sp.getOutputStream().flush();
System.out.println("Sent number: " + i);
Thread.sleep(1000);
}
if (sp.closePort()) {
System.out.println("Port is closed :)");
} else {
System.out.println("Failed to close port :(");
return;
}
}
}
Мы видим, что код является полностью блокирующим (т.к. мы заранее знаем объём передаваемых данных, когда в цикле с компьютера пересылаем одну цифру на Arduino). Кроме того, следующая строка даёт нам хороший пример того, как нужно конфигурировать порт для общения с Arduino:
sp.setComPortParameters(9600, 8, 1, 0);
Также можно легко заметить (так как инициатором является компьютер), что в этом случае мы используем исходящий с компьютера поток (OutputStream):
sp.getOutputStream().write(i.byteValue());
sp.getOutputStream().flush();
Перейдём к стороне Arduino. Здесь всё достаточно стандартно, и для чтения используется простая конструкция:
if (Serial.available() > 0) {
byte incomingByte = 0;
incomingByte = Serial.read();
}
Случай, когда Arduino шлёт данные на компьютер
А теперь попробуем рассмотреть обратный вариант, когда необходимо общаться, так сказать, в «восходящем» режиме — то есть Arduino шлёт сообщения компьютеру.
Для этого нам надо внести изменения в Java код, а также изменить скетч прошивки Arduino.
Начнём с кода Java. Во-первых, так как в этом случае нужно принимать поток данных, нам необходимо изменить getOutputStream на getInputStream:
byte b= (byte) sp.getInputStream().read();
Также нам необходимо изменить режим записи — на режим чтения, переключив его на полублокирующий вариант. Как можно увидеть в строке ниже, полублокирующий режим у нас включён таким образом, что мы ждём, пока хотя бы 1 байт данных не будет успешно прочитан (это нули, следующие за выражением TIMEOUT_READ_SEMI_BLOCKING):
sp.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 0, 0);
И весь код будет выглядеть следующим образом:
import com.fazecast.jSerialComm.*;
import java.io.*;
import java.util.ArrayList;
/**
* Based on the tutorial example,
* showing how to establish a serial connection between
* a Java and Arduino program:
* @author Michael Schoeffler (www.mschoeffler.de)
*
*/
public class Main {
public static void main(String[] args) throws IOException, InterruptedException
{
SerialPort sp = SerialPort.getCommPorts()[1];
sp.setComPortParameters(9600, 8, 1, 0); // настройки для Arduino
sp.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 0, 0);
if (sp.openPort()) {
System.out.println("Port is open :)");
} else {
System.out.println("Failed to open port :(");
return;
}
//в бесконечном цикле слушаем порт
while(true)
{
byte b= (byte) sp.getInputStream().read();
System.out.println("Received data: " + b);
}
}
}
Как можно заметить из кода, мы сделали так, что поступающие данные из COM-порта читаются в бесконечном цикле.
Скетч Arduino, в свою очередь, достаточно прост и выглядит следующим образом:
byte b = 23;
void setup() {
Serial.begin (9600);
}
void loop() {
delay(1000);
Serial.write(b);
}
Как можно заметить, с периодичностью в одну секунду мы просто пишем байты в COM-порт.
Для чего это всё можно применить?
А теперь попробуем прикинуть, для чего, собственно, вся эта последовательность телодвижений может пригодиться?
Понятно, что способ общения Arduino с компьютером является весьма полезным для целой массы применений, среди которых могут быть и разнообразные погодные станции, и управление системами робототехники и т. д.
Лично мой кейс использования выглядит приблизительно так (для чего, собственно, я это всё и затеял. Уже жду детали из Китая):
Дело в том, что я уже достаточно давно (лет 10) для просмотра телевизора и фильмов, использую видеопроектор. Телевизионный сигнал идёт с подключаемой приставки, а фильмы я смотрю с компьютера. Чтобы мне не нужно было постоянно перетыкать HDMI-кабель в гнездо видеопроектора — я купил специальный разветвитель, который после нажатия кнопки переключает один источник сигнала на другой (это всё можно было и не писать, но так ситуация будет более понятна).
Периодически, когда я смотрю телевизор, я натыкаюсь на ряд фильмов, которые идут по телевизору в SD-качестве. Несмотря на то, что я эти фильмы уже видел 100500 раз — мне всё равно хочется пересматривать их ещё.
В этот момент я вспоминаю, что доступ к этим фильмам есть у меня на компьютере и в отличном качестве. После чего — я иду к компьютеру, включаю фильм, возвращаюсь обратно, переключаю источник сигнала и смотрю фильм уже в хорошем качестве.
То есть, если бы у меня был некий способ, не вставая с дивана включать фильм на компьютере — я вполне мог бы начать смотреть его с минимальными усилиями и без лишних походов.
Тут многие, наверное, скажут: «да купи ты себе беспроводную мышь и не парься!»
На это у меня есть что ответить: раньше так и было, однако однажды у меня родилась мысль, как можно упростить это ещё сильнее!
В одной из прошлых статей мы уже рассматривали способ, как можно подключить к своей Arduino любой имеющийся под рукой инфракрасный пульт дистанционного управления.
Суть этого способа в двух словах заключается в следующем:
- мы подключаем приёмник инфракрасного излучения к Arduino,
- загружаем в Arduino специальный скетч, после чего нажимаем на любую нужную кнопку пульта дистанционного управления (например, на ту кнопку, которая у нас обычно ни в чём не задействована). При нажатии на эту кнопку пульт излучает определённый код, который передаётся на Arduino, и отображается в мониторе порта. Записываем этот код (например, на бумажку),
- далее мы загружаем в Arduino другой скетч, который является уже исполнителем определённых действий, при получении этого кода на инфракрасный приёмник.
Таким образом, совместив между собой эти два способа (то есть приём и обработку кодов нажатий определённых кнопок на пульте), на подключённой к компьютеру Arduino — с Java программой компьютере, мы можем управлять с инфракрасного пульта процессами, протекающими на компьютере!
Например, в моём случае, я хочу запускать воспроизведение фильмов на компьютере.
Тут следует сделать одну оговорку: дело в том, что пульт управления от видеопроектора у меня неродной (родной вышел из строя) и поэтому я использую программируемый универсальный пульт, в котором у меня используется, дай бог, если 35% от имеющихся на нём кнопок.
Получается, я вполне могу назначить на оставшиеся кнопки весьма широкий набор функций: добавить громкость, убавить громкость, включить воспроизведение, поставить на паузу, перемотка, переключить на другой видеофайл и т.д. и т.п.
Весь проект со стороны компьютера я вижу следующим образом: в компьютер (всегда в один и тот же USB-разъём) будет воткнута самая маленькая Arduino, которая у меня имеется, — Arduino Nano, с подключённым к ней инфракрасным приёмником. Всё это будет выполнено достаточно компактно — в форм-факторе флешки, а корпус будет изготовлен 3D печатью.
Сама java-программа будет добавлена в автозагрузку компьютера.
Скетч для Arduino будет максимально простым. Приблизительно похожим на тот, что вы уже видели выше.
В завершение хочу сказать, что я таким образом собираюсь несколько увеличить комфорт своего просмотра фильмов и телевизора на досуге, а вы же — можете использовать этот способ для каких-то своих, более глобальных целей.
Если подумать, применений этому способу может быть очень много. С ходу мне в голову приходит изготовление некоего «презентера», который позволяет запускать файлы на компьютере и выводить их на большой экран во время выступлений на сцене перед большой аудиторией. Это всего лишь одна из идей. А так, думаю, этот способ может быть многим полезен.
Удачи всем в сборке!
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.