Вернуться на главную страницу

NetBeans, Raspberry Pi и Arduino

Вступление

В данной статье я хочу показать как в NetBeans'е собирать, запускать и отлаживать C++ программы на удалённом хосте. Сегодня мы не будем трогать мощные Sparc сервера, а вспомним анекдот из начала девяностых.

- Угадайте, что у меня в спрятано в руке
- ... Правильно, компьютер. А сколько?

Так что будем считать, что в одной руке у меня Raspberry Pi, а в другой Arduino. Думаю, что этого достаточно для демонстрации основных возможностей NetBeans. Попутно мы должны научиться взаимодействовать с внешней web-страницей и оснастить Raspberry Pi дисплеем. Думаю, что прочитанного будет достаточно, чтобы читатель собрал "устройство" управляемое пультом дистанционного управления, считывающее показания с сенсора (термометра). Ну или мигающее светодиодом. Как же без этого. Выглядеть это может так:

Arduino+RaspberryPi

Чтобы сократить статью, я буду полагать, что читатели знакомы с C/C++ плагином для NetBeans, умеют пользоваться Raspberry Pi, программировали под Linux и что-то читали про Arduino.

Режимы работы с удалённой машиной из NetBeans

Попробую перечислить все возможные варианты:
- работа в терминале через ssh (Наверное всем хорошо известный способ управления локальной или удалённой машиной, но в NetBeans терминал работает на ограниченном множестве платформ. Linux/ARM не поддерживается, так что на Raspberry Pi он не заработает.
- проводник по файлам на удалённой машине (Если всё настроить, то в табе Favorites можно копировать, создавать и редактировать файлы на удалённой машине как в локальном проводнике файлов)
- кросс-компиляция (Довольно популярный способ разработки. Можно сидеть на Windows машине, но собирать код для Linux/ARM. Запустить его, конечно, можно или под эмулятором или скопировав на целевую машину. Что-то такое мы получим, когда будем работать с Arduino.)
- общая файловая система (Используя Samba или NFS, пользователь может настроить так файловую систему, что все файлы в папке будут доступны как локальные файлы на обеих машинах. В таком случаи среде разработки не нужно копировать файлы, а достаточно только вызывать сборку на удалённой машине.)
- автоматическое копирование (Этот режим должен быть удалён, но пока ещё доступен. При всех своих плюсах он наиболее непонятен пользователям. NetBeans не копирует файлы, а создаёт файлы забитые нулями, что экономит время на загрузке файлов. При сборке, если NetBeans видит что файл устарел, то содержимое файла подменяется на новое. Этот режим тоже не работает на Linux/ARM.)
- SFTP (В этом режиме папка проекта пакуется, копируется на удалённую машину и распаковывается. Для небольших проектов удобно, а для больших проектов может быть медленно.)
- полностью удалённая работа (Всё делается на удалённой машине. На локальной машине сохраняются только локальные кэши.)

Настройка Raspberry Pi

Чтобы можно было достучаться до Rapberry Pi, SSH сервер должен быть запущен. Нам точно понадобятся gcc, g++, gdb и make. Так что установим их: sudo apt-get install gcc g++ gdb make. В моих примерах я буду использовать библиотеку curl. Так что установим ещё: sudo apt-get install libcurl4-openssl-dev. Для работы с Arduino нужно ещё поставить 3 пакета: sudo apt-get install arduino arduino-core arduino-mk.

Добавление нового хоста в NetBeans

По умолчанию, так как на панели инструментов мало места, нужные нам кнопки скрыты. Сделаем их доступными. Вызовем контекстное меню на панели инструментов и выберем пункт "Remote". В появившемся выпадающем списке выберем "Manage Hosts" и нажмём кнопку Add. Введём IP адрес Raspberry Pi, логин и пароль на нём. На третьем шаге нужно выбрать режим доступа. Выберем SFTP, хотя в данной статье этот пункт ничего не значит, так как мы будем использовать так называемый FullRemote режим.

Remote toolbar

Настройка компиляторов

В окне настроек NetBeans'а должен появиться новый хост и GCC компилятор. Но мы добавим ещё GCC-AVR. Нажмём кнопку "Add..." и выберем /usr/bin. Среда разработки должна предложить выбрать GNU как набор тулов, а уж имя можно использовать любое. Я выбрал GNU_AVR. После нажатия на кнопку OK нужно будет подправить получившиеся настройки. Поля Fortran, отладчик и QMake нужно очистить. А gcc, g++ и as заменить на avr-gcc, avr-g++ и avr-as.

Build tools

Теперь неплохо бы проверить Services таб и убедиться, что всё правильно настроено. Ваши настройки должны совпасть с моей картинкой.

Services tab

Настройка проводника по файлам на удалённой машине

Для копирования, удаления и создания файлов на удалённой машине можно добавить рабочую директорию в таб Favorites. В табе Services нужно найти имя или IP-адрес в разделе "C/C++ Build Hosts" и вызвать контекстное меню. Искомый пункт меню - "Add to Favorites".

Favorites tab

Встроенный терминал

Терминал вызывается через Window > IDE Tools > Terminal. Для добавления новой машины с левой стороны у таба есть кнопка "Create New Remote Terminal Tab". Но Linux/ARM не поддерживается (хотя у меня терминал работает в NetBeans 8.0.2). Можно, правда, набрать команду "ssh user@host" в текущем локальном терминале, но в таком случаи интеграции со средой разработки у нас не будет.

Terminal

Чтение строки с web-страницы

Давайте напишем простенькую программу. Предположим, что у вас где-то есть web-сервер и вы хотите содержимое какой-то страницы вывести, например, на дисплей. Но сначала просто распечатаем значение в терминале. Чтобы создать проект на удалённой машине мы должны выбрать только что добавленный хост в выпадающем списке на панели инструментов и нажать кнопку "Create Remote C/C++ Project". Создадим C++ проект с файлом main.cpp. Мы будем использовать библиотеку libcurl, так что откроем свойства проекта и нажмём на кнопку с 3 точками в секции Build|Linker|Libraries. В появившемся окне нажмём "Add PkgConfig Library..." и выберем libcurl.

Pkg-Config

Теперь добавим код.

main.cpp (пример программы, которая читает web-страничку и печатает содержимое между звёздочками) [посмотреть/скрыть код]

#include <iostream>
#include <cstring>
#include <string>
#include <curl/curl.h>

using namespace std;

/**
 * Что-нибудь сделать с полученной с web-странички строкой
 *
 * @param command
 */
void doAction(const string& command) {
    cout << command << endl;
}

/**
 * Функция сохраняет web страницу в переменную
 *
 */
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
    (static_cast<string*> (userp))->append(static_cast<const char*> (contents));
    return size * nmemb;
}

int main(void) {
    const char* url = "http://192.168.1.117:8084/raspberrypi.html";
    const char* user_agent = "libcurl-agent/1.0";
    const char* separatorBegin = "<p>";
    const char* separatorEnd = "</p>";

    /* более или менее универсальный код (настройка библиотеки и чтение страницы) */
    CURL *curl_handle;
    CURLcode res;
    string content;
    curl_global_init(CURL_GLOBAL_ALL);
    curl_handle = curl_easy_init();
    curl_easy_setopt(curl_handle, CURLOPT_URL, url);
    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, static_cast<void *> (&content));
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, user_agent);
    res = curl_easy_perform(curl_handle);

    /* анализируем содержимое страницы */
    if (res == CURLE_OK) {
        size_t len = strlen(separatorBegin);
        size_t begin = content.find(separatorBegin);
        size_t end = content.find(separatorEnd);
        if ((begin != string::npos) && (end != string::npos)) {
            begin += len;
            if (end >= begin) {
                doAction(content.substr(begin, end - begin));
            }
        }
    } else {
        cerr << "ERROR!" << endl;
    }

    /* более или меее универсальный код */
    curl_easy_cleanup(curl_handle);
    curl_global_cleanup();

    return 0;
}

А вот такого вида страницу нужно закинуть на свой вэб-сервер.

raspberrypi.html (содержимое страницы, что мы будем читать) [посмотреть/скрыть код]
<html>
<body>
<p>Test page</p>
</body>
</html>

Запустим проект из NetBeans и проверим, что программа работает и печатает "Test page". Включив фантазию легко представить, что слегка изменив программу, мы пошлём на внешний вэб-сервер процент загрузки процессора на Raspberry Pi или данные с какого-нибудь датчика. А читать мы тоже можем, что-то более важное и сложное.

Убедимся, что редактор в удалённом режиме работает также как и в локальном (справка по функциям, анализ заголовочных файлов и т.п.).

Remote editor

Remote editor

Теперь запустим отладчик и получим ошибку. Не стоит бояться - это не ошибка NetBeans (например). Нужно просто создать .gdbinit файл на Raspberry Pi с таким содержанием:
handle SIGILL pass nostop noprint

Вновь запустим отладчик и убедимся, что он работает как и на локальной машине.

Remote debugger

Подключение Arduino (MicroView)

На мой взгляд для демонстраций очень подходит MicroView. Это и Arduino и компактное устройство и дисплей. Так что подключив 1-2 датчика вы не запутаетесь в проводах. На самой первой картинке можно увидеть MicroView, трёхцветный светодиод, датчик температуры и давления, а также инфракрасный приёмник для управления устройством при помощи телевизионного пульта управления.

Если мы говорим про простых ардуинщиков, то в этом мире принято, что программы имеют расширение .ino. Это фактически C++ файлы, но NetBeans этого не знает. Так что первым делом нужно добавить ino в список файлов, что среда разработки будет рассматривать как C++ код.

.ino extension

Далее на RaspberryPi нужно создать папку sketchbook в своей домашней директории. А в ней нужно создать папку libraries. Все библиотеки для работы с датчиками нужно будет копировать сюда. Для работы с MicroView скачаем родную библиотеку. Теперь, когда у нас есть файл ~/sketchbook/libraries/MicroView/MicroView.h, мы создадим наше тестовое приложение. Создадим папку ~/sketchbook/display и скопируем туда 2 файла Makefile и display.ino. Необходимо отметить, что различные Arduino устройства требуют разные BOARD_TAG и ARDUINO_PORT. Так что ваш Makefile может отличаться от моего.

Makefile [посмотреть/скрыть код]
BOARD_TAG     = uno
ARDUINO_DIR = /usr/share/arduino
ARDUINO_LIBS = MicroView
ARDUINO_PORT  = /dev/ttyUSB0

include /usr/share/arduino/Arduino.mk

display.ino [посмотреть/скрыть код]
#include <MicroView.h> // библиотека для работы с платформой MicroView

String line = ""; // что показывать на дисплее
boolean completed = false; // если рано true, то обновить дисплей

void setup() {
    Serial.begin(9600);
    line.reserve(250);
    uView.begin(); // Начало работы с MicroView
    uView.clear(PAGE); // очищаем дисплей
    uView.print(""); // печатаем строку в буфер дисплея
    uView.display(); // отображаем содержимое из буфера на экран
}

void loop() {
    if (completed) { // пора обновлять дисплей
        uView.clear(PAGE);
        uView.setCursor(0, 0);
        uView.print(line);
        uView.display();
        line = "";
        completed = false;
    }
    delay(100);
}

void serialEvent() {
    while (Serial.available()) { // пришли новые данные
        char ch = (char) Serial.read();
        line += ch;
        if (ch == '\n') {
            completed = true;
        }
    }
}

Теперь создадим ещё один проект на удалённой машине, но теперь выберем "C/C++ Project with Existing Sources" как тип проекта и GNU_AVR как набор компиляторов. В созданном проекте файл display.ino помечен серым цветом, что значит что NetBeans не анализирует код в этом файле. Содержимое папки build-cli будет тоже только мешать. Вызовем окно свойсв проекта и заменим ^(nbproject)$ на ^(nbproject|build-cli)$ в поле "Ignored Folders Pattern". А в свойствах файла (нужно вызвать контекстное меню на файле и выбрать Properties) поставить галочку в поле "Add To Parse".

Проделав всё выше сказанное мы получим проект с двумя файлами (папку Important Files и его содержимое мы считать не будем), но весь код в файле display.ino помечен красным цветом. Пересоберём проект ещё раз, но обратим внимание как компилятор на самом деле компилирует код. Найдём такую или похожую строку и скопируем её в буфер обмена: /usr/bin/avr-g++ -MM -mmcu=atmega328p -DF_CPU=16000000L -DARDUINO=100 -I. -I/usr/share/arduino/hardware/arduino/cores/arduino -I/usr/share/arduino/hardware/arduino/variants/standard -I/usr/share/arduino/libraries/MicroView -I/home/sova/sketchbook/libraries/MicroView -g -Os -w -Wall -ffunction-sections -fdata-sections -fno-exceptions build-cli/display.cpp -MF build-cli/display.d -MT build-cli/display.o

А сейчас откроем свойства проекта, перейдём в категорию Code Assistance|C++ Compiler и вставим содержимое буфера обмена в поля "Include Directories" и "Preprocessor Definitions". NetBeans постарается вытянуть нужные значения, но, тем не менее, там останется много мусора. Так что нужно подредактировать получившиеся настройки (кнопка с тремя точками рядом с редактируемым полем). Мои значения: /usr/share/arduino/hardware/arduino/cores/arduino;/usr/share/arduino/hardware/arduino/variants/standard;/home/sova/sketchbook/libraries/MicroView и F_CPU=16000000L ARDUINO=100

По этому примеру не заметно (MicroView.h сам подключает нужный файл), но все Arduino программы требуют подключения файла Arduino.h. Просто в *.ino файлах это делается неявно. Мы же скопируем полный путь к заголовочному файлу в поле "Include Headers". В моём случаи это /usr/share/arduino/hardware/arduino/cores/arduino/Arduino.h

Если мы оставим настройки такими, то редактор пометит все строки с Serial как ошибочные. Легко понять, что флаг компилятора -mmcu=atmega328p важен. Он включает макрос __AVR_ATmega328P__ и только в этом случаи корректно понимается /usr/lib/avr/include/avr/io.h файл. Если у вас другой процессор, то у вас будет другой макрос. Но вы можете использовать мой, так как эти настройки не передаются компилятору. Категория "Code Assistance" - это настройки редактора.

Так что окончательное значение в поле "Preprocessor Definitions": F_CPU=16000000L ARDUINO=100 __AVR_ATmega328P__=1

Arduino API

После всех манипуляций у меня работает редактирование и пересборка. Единственное - я не могу загрузить программу на MicroView. "make upload" падает с такой ошибкой:

cd '/home/sova/sketchbook/display'
/usr/bin/make -f Makefile upload
cat build-cli/display.d > build-cli/depends.mk
for STTYF in 'stty -F' 'stty --file' 'stty -f' 'stty <' ; \
	  do $STTYF /dev/tty >/dev/null 2>&1 && break ; \
	done ; \
	$STTYF /dev/ttyUSB0  hupcl ; \
	(sleep 0.1 2>/dev/null || sleep 1) ; \
	$STTYF /dev/ttyUSB0 -hupcl 
stty: стандартный ввод: Неприменимый к данному устройству ioctl
stty: стандартный ввод: Неприменимый к данному устройству ioctl
/usr/share/arduino/Arduino.mk:503: ошибка выполнения рецепта для цели «reset»
make: *** [reset] Ошибка 1

BUILD FAILED (exit value 2, total time: 437ms)

Так что приходиться подключаться с помощью ssh (пусть и используя встроенный в NetBeans терминал) и выполнять загрузку вручную.

Arduino code

Пример использования

У меня Raspberry Pi не подключён к дисплею, так что мне нужно знать его IP адрес для того чтобы зайти через SSH. Можно, например, использовать такой трюк. В /etc/rc.local я добавил 2 строки. "/bin/stty -F /dev/ttyUSB0 -hupcl" - автоматически не закрывать устройство /dev/ttyUSB0, "/root/display.sh &" - в фоне запустить скрипт. Скрипт ждёт 10 секунд (без паузы не будет работать дисплей, так как что-то не успевает инициализироваться), получает ip адрес машины и пишет его в /dev/ttyUSB0. А уж Arduino его отображает на дисплее.

/etc/rc.local [посмотреть/скрыть код]
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
  /bin/stty -F /dev/ttyUSB0 -hupcl
  /root/display.sh &
fi

exit 0
/root/display.sh [посмотреть/скрыть код]
#!/bin/sh -e
sleep 10
hostname -I > /dev/ttyUSB0
exit 0

© 2008 - 2015 Солдатов Валерий Фёдорович
Ваши комментарии и замечания.