Картинка 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. Рассмотрим компьютерную сторону:

Java-код для компьютера
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 любой имеющийся под рукой инфракрасный пульт дистанционного управления.

Суть этого способа в двух словах заключается в следующем:

  1. мы подключаем приёмник инфракрасного излучения к Arduino,
  2. загружаем в Arduino специальный скетч, после чего нажимаем на любую нужную кнопку пульта дистанционного управления (например, на ту кнопку, которая у нас обычно ни в чём не задействована). При нажатии на эту кнопку пульт излучает определённый код, который передаётся на Arduino, и отображается в мониторе порта. Записываем этот код (например, на бумажку),
  3. далее мы загружаем в Arduino другой скетч, который является уже исполнителем определённых действий, при получении этого кода на инфракрасный приёмник.

Таким образом, совместив между собой эти два способа (то есть приём и обработку кодов нажатий определённых кнопок на пульте), на подключённой к компьютеру Arduino — с Java программой компьютере, мы можем управлять с инфракрасного пульта процессами, протекающими на компьютере!

Например, в моём случае, я хочу запускать воспроизведение фильмов на компьютере.

Тут следует сделать одну оговорку: дело в том, что пульт управления от видеопроектора у меня неродной (родной вышел из строя) и поэтому я использую программируемый универсальный пульт, в котором у меня используется, дай бог, если 35% от имеющихся на нём кнопок.

Получается, я вполне могу назначить на оставшиеся кнопки весьма широкий набор функций: добавить громкость, убавить громкость, включить воспроизведение, поставить на паузу, перемотка, переключить на другой видеофайл и т.д. и т.п.

Весь проект со стороны компьютера я вижу следующим образом: в компьютер (всегда в один и тот же USB-разъём) будет воткнута самая маленькая Arduino, которая у меня имеется, — Arduino Nano, с подключённым к ней инфракрасным приёмником. Всё это будет выполнено достаточно компактно — в форм-факторе флешки, а корпус будет изготовлен 3D печатью.

Сама java-программа будет добавлена в автозагрузку компьютера.

Скетч для Arduino будет максимально простым. Приблизительно похожим на тот, что вы уже видели выше.

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

Если подумать, применений этому способу может быть очень много. С ходу мне в голову приходит изготовление некоего «презентера», который позволяет запускать файлы на компьютере и выводить их на большой экран во время выступлений на сцене перед большой аудиторией. Это всего лишь одна из идей. А так, думаю, этот способ может быть многим полезен.

Удачи всем в сборке!


НЛО прилетело и оставило здесь промокод для читателей нашего блога:

15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.

Комментарии (2)


  1. Serge78rus
    17.08.2022 12:59

    Для управления компьютером с помощью различных инфракрасных пультов ДУ есть же LIRC. Так же есть порт для Винды — WinLIRC.


  1. Teapot
    17.08.2022 17:04

    Кстати, можно взять Liberica JDK Embedded
    https://bell-sw.com/pages/libericajdk-for-embedded/
    и для операций с COM-портом использовать готовый UART из модуля DIO:
    https://docs.oracle.com/javame/8.2/api/dio/api/jdk/dio/uart/UART.html