Освой Arduino играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Связываемся с Processing

Получение строковых данных из микроконтроллера
Получение числовых данных из микроконтроллера
04.Communication: Dimmer
04.Communication: PhysicalPixel (Зажигаем светодиод мышкой)
04.Communication: Graph (Рисуем график)
Управляем цифровыми пинами (черновик)
Используем кнопки из ControlP5

При работе с платой Arduino мы иногда выводим результат на Serial Monitor. Но это не единственная возможность для получения данных на экране. Например, вы можете воспользоваться программой Processing.

Когда вы установите эту программу, то удивитесь - насколько она похожа на Arduino IDE. Не удивляйтесь, обе программы сделаны на одном движке.

Приложение Processing имеет очень много возможностей, в том числе и библиотеку Serial, поэтому мы можем связать передачу данных между платой и Processing.

Запустим Arduino IDE и выберем простейший пример вывода данных на Serial Port:


void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println("Hello Kitty!");
  // ждем 500 миллисекунд перед следующей отправкой
  delay(500);
}

Запустим пример и убедимся, что код работает.

Получение строковых данных из микроконтроллера

Теперь мы хотим получить этот же текст в Processing. Запускаем новый проект и напишем код.

Первый шаг - импортировать библиотеку. Идем в Sketch | Import Library | Serial. В скетче появится строка:


import processing.serial.*;

Далее объявляем переменные, создаём обязательные функции. Обратите внимание, что в отличии от скетчей Arduino, в скетчах Processing используется функция draw() вместо loop().


import processing.serial.*;

Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта

void setup()
{
  String port = Serial.list()[0];
  serial = new Serial(this, port, 9600);
}

void draw() {

  if ( serial.available() > 0) { // если есть данные,
    received = serial.readStringUntil('\n'); // считываем данные
  }
  println(received); //отображаем данные в консоли
}

Чтобы обеспечить прием данных с последовательного порта, нам нужен объект класса Serial. Так как с Arduino мы отправляем данные типа String, нам надо получить строку и в Processing.

В методе setup() нужно получить доступный последовательный порт. Как правило, это первый доступный порт из списка. После этого мы можем настроить объект Serial, указав порт и скорость передачи данных (желательно, чтобы скорости совпадали).

Осталось снова подключить плату, запустить скетч от Processing и наблюдать поступаемые данные в консоли приложения.

Processing

Processing позволяет работать не только с консолью, но и создавать стандартные окна. Перепишем код.


import processing.serial.*;

Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта

void setup()
{
  size(320, 120);
  String port = Serial.list()[0];
  serial = new Serial(this, port, 9600);
}

void draw() {

  if ( serial.available() > 0) { // если есть данные,
    // считываем их и записываем в переменную received
    received = serial.readStringUntil('\n');
  }

  // Настройки для текста
  textSize(24);
  clear();
  if (received != null) {
    text(received, 10, 30);
  }
}

Запустим пример ещё раз и увидим окно с надписью, которое перерисовывается в одном месте.

Processing

Таким образом мы научились получать данные от Arduino. Это позволит нам рисовать красивые графики или создавать программы контроля за показаниями датчиков.

Получение числовых данных из микроконтроллера

Получение строк во многих случаях избыточно. Нам вполне хватит пары чисел 0 и 1, чтобы узнать, что кнопка нажата или нет. Возьмём пример 01.Basics: DigitalReadSerial (Чтение цифрового вывода) и немного модифицируем его.


int pushButton = 7;

void setup() {
  Serial.begin(9600);
  pinMode(pushButton, INPUT);
}

void loop() {
  if (digitalRead(pushButton) == HIGH) {
    Serial.write(1);
  } else {
    Serial.write(0);
  }
  delay(100); // задержка для стабильности
}

Мы посылаем состояние кнопки. Осталось написать код для Processing. Создадим окно с квадратом. В обычном состоянии квадрат будет серым, при нажатии кнопки он станет зелёным.



{
  size(200, 200);
  String port = Serial.list()[0];
  serial = new Serial(this, port, 9600);
}

void draw() {
  if ( serial.available() > 0) { // если есть данные,
    received = serial.read(); // считываем данные
  }
  println(received); //отображаем данные в консоли
  background(255);
  if (received == 0) {
    fill(122);
  } else {
    fill(0, 255, 0);
  }
  rect(50, 50, 100, 100);
}

Отправка данных

Мы можем не только получать данные с платы, но и отправлять данные на плату, заставляя выполнять команды с компьютера.

Допустим, мы будем посылать символ "1" из Processing. Когда плата обнаружит присланный символ, включим светодиод на порту 13 (встроенный).

Скетч будет похож на предыдущий. Для примера создадим небольшое окно. При щелчке в области окна будем отсылать "1" и дублировать в консоли для проверки. Если щелчков не будет, то посылается команда "0".


import processing.serial.*;

Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта

void setup()
{
  size(320, 120);
  String port = Serial.list()[0];
  serial = new Serial(this, port, 9600);
}

void draw() {

  if (mousePressed == true) { 
    //если мы кликнули мышкой в пределах окна
    serial.write('1'); //отсылаем 1
    println("1");
  } else { //если щелчка не было
    serial.write('0'); //отсылаем 0
  }
}

Теперь напишем скетч для Arduino.


char commandValue; // данные, поступаемые с последовательного порта
int ledPin = 13; // встроенный светодиод

void setup() {
  pinMode(ledPin, OUTPUT); // режим на вывод данных
  Serial.begin(9600);
}

void loop() {
  if (Serial.available()) {
    commandValue = Serial.read();
  }

  if (commandValue == '1') {
    digitalWrite(ledPin, HIGH); // включаем светодиод
  }
  else {
    digitalWrite(ledPin, LOW); // в противном случае выключаем
  }
  delay(10); // задержка перед следующим чтением данных
}

Запускаем оба скетча. Щёлкаем внутри окна и замечаем, что светодиод загорается. Можно даже не щёлкать, а удерживать кнопку мыши нажатой - светодиод будет гореть постоянно.

Обмен данными

Теперь попытаемся объединить оба подхода и обмениваться сообщениями между платой и приложением в двух направлениях.

Для максимальной эффективности добавим булеву переменную. В результате у нас отпадает необходимость постоянно отсылать 1 или 0 от Processing и последовательный порт разгружается и не передает лишнюю информацию.

Когда плата обнаружит присланную единицу, то меняем булевое значение на противоположное относительно текущего состояния (LOW на HIGH и наоборот). В else используем строку "Hello Kity", которую будем отправлять только в случае, когда не обнаружим '1'.

Функция establishContact() отсылает строку, которую мы ожидаем получить в Processing. Если ответ приходит, значит Processing может получить данные.


char commandValue; // данные, поступаемые с последовательного порта
int ledPin = 13;
boolean ledState = LOW; //управляем состоянием светодиода

void setup() {
  pinMode(ledPin, OUTPUT);
  Serial.begin(9600);
  establishContact(); // отсылаем байт для контакта, пока ресивер отвечает
}

void loop() {
  // если можно прочитать данные
  if (Serial.available() > 0) {
    // считываем данные
    commandValue = Serial.read();
    if (commandValue == '1') {
      ledState = !ledState;
      digitalWrite(ledPin, ledState);
    }
    delay(100);
  }
  else {
    // Отсылаем обратно
    Serial.println("Hello Kitty");
  }
  delay(50);
}

void establishContact() {
  while (Serial.available() <= 0) {
    Serial.println("A"); // отсылает заглавную A
    delay(300);
  }
}

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

Добавим новую булеву переменную firstContact, которая позволяет определить, есть ли соединение с Arduino.

В методе setup() добавляем строку serial.bufferUntil('\n');. Это позволяет хранить поступающие данные в буфере, пока мы не обнаружим определённый символ. В этом случае возвращаем (\n), так как мы отправляем Serial.println() от Arduino. '\n' в конце значит, что мы активируем новую строку, то есть это будут последние данные, которые мы увидим.

Так как мы постоянно отсылаем данные, метод serialEvent() выполняет задачи цикла draw(), то можно его оставить пустым.

Теперь рассмотрим основной метод serialEvent(). Каждый раз, когда мы выходим на новую строку (\n), вызывается этот метод. И каждый раз проводится следующая последовательность действий:

  • Считываются поступающие данные;
  • Проверяется, содержат ли они какие-то значения (то есть, не передался ли нам пустой массив данных или "нуль");
  • Удаляем пробелы;
  • Если мы первый раз получили необходимые данные, изменяем значение булевой переменной firstContact и сообщаем Arduino, что мы готовы принимать новые данные;
  • Если это не первый приём необходимого типа данных, отображаем их в консоли и отсылаем микроконтроллеру данные о клике, который совершался;
  • Собщаем Arduino, что мы готовы принимать новый пакет данных.


import processing.serial.*;

Serial serial; // создаем объект последовательного порта
String received; // данные, получаемые с последовательного порта
// Проверка на поступление данных от Arduino
boolean firstContact = false;

void setup()
{
  size(320, 120);
  String port = Serial.list()[0];
  serial = new Serial(this, port, 9600);
  serial.bufferUntil('\n');
}

void draw() {
}

void serialEvent( Serial myPort) { //формируем строку из данных, которые поступают

  // '\n' - разделитель - конец пакета данных

  received = myPort.readStringUntil('\n'); //убеждаемся, что наши данные не пустые перед тем, как продолжить

  if (received != null) { //удаляем пробелы

    received = trim(received);

    println(received); //ищем нашу строку 'A' , чтобы начать рукопожатие

    //если находим, то очищаем буфер и отсылаем запрос на данные
    if (firstContact == false) {
      if (received.equals("A")) {
        serial.clear();
        firstContact = true;
        myPort.write("A");
        println("contact");
      }
    } else { //если контакт установлен, получаем и парсим данные
      println(received);

      if (mousePressed == true) { //если мы кликнули мышкой по окну
        serial.write('1'); //отсылаем 1
        println("1");
      } // когда вы все данные, делаем запрос на новый пакет
      serial.write("A");
    }
  }
}

При подключении и запуске в консоли должна появится фраза 'Hello Kitty'. Когда вы будете щёлкать мышкой в окне Processing, светодиод на пине 13 будет включаться и выключаться.

Кроме Processing, вы можете использовать программы PuTTy или написать свою программу на C# использованием готовых классов для работы с портами.

04.Communication: Dimmer

Пример демонстрирует, как можно посылать данные из компьютера на плату для управления яркостью светодиода. Данные поступают в виде отдельных байтов от 0 до 255. Данные могут поступать от любой программы на компьютере, которая имеет доступ к последовательному порту, в том числе от Processing.

Для примера понадобится стандартная схема с резистором и светодиодом на выводе 9.

Скетч для Arduino.


const int ledPin = 9;      // светодиод на выводе 9

void setup() {
  Serial.begin(9600);
  // устанавливаем режим на вывод
  pinMode(ledPin, OUTPUT);
}

void loop() {
  byte brightness;

  // проверяем, есть ли данные от компьютера
  if (Serial.available()) {
    // читаем последние полученные байты от 0 до 255
    brightness = Serial.read();
    // устанавливаем яркость светодиода
    analogWrite(ledPin, brightness);
  }
}

Код для Processing


import processing.serial.*;
Serial port;

void setup() {
  size(256, 150);

  println("Available serial ports:");
  println(Serial.list());

  // Uses the first port in this list (number 0). Change this to select the port
  // corresponding to your Arduino board. The last parameter (e.g. 9600) is the
  // speed of the communication. It has to correspond to the value passed to
  // Serial.begin() in your Arduino sketch.
  port = new Serial(this, Serial.list()[0], 9600);

  // Если вы знаете имя порта, используемой платой Arduino board, то явно укажите
  //port = new Serial(this, "COM1", 9600);
}

void draw() {
  // рисуем градиент от чёрного к белому
  for (int i = 0; i < 256; i++) {
    stroke(i);
    line(i, 0, i, 150);
  }

  // записывем текущую X-позицию мыши в последовательный порт как байт
  port.write(mouseX);
}

Запускаем Processing и водим мышкой над созданным окном в любую сторону. При движении влево яркость светодиода будет уменьшаться, при движении вправо - увеличиваться.

Dimmer

04.Communication: PhysicalPixel (Зажигаем светодиод мышкой)

Немного изменим задачу. Будем проводить мышкой над квадратом и посылать символ "H" (High), чтобы зажечь светодиод на плате. Когда мышь покинет область квадрата, то пошлём символ "L" (Low), чтобы погасить светодиод.

Код для Arduino.


const int ledPin = 13; // вывод 13 для светодиода
int incomingByte;      // переменная для получения данных

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // если есть данные
  if (Serial.available() > 0) {
    // считываем байт в буфере
    incomingByte = Serial.read();
    // если это символ H (ASCII 72), то включаем светодиод
    if (incomingByte == 'H') {
      digitalWrite(ledPin, HIGH);
    }
    // если это символ L (ASCII 76), то выключаем светодиод
    if (incomingByte == 'L') {
      digitalWrite(ledPin, LOW);
    }
  }
}

Код для Processing.


import processing.serial.*;

float boxX;
float boxY;
int boxSize = 20;
boolean mouseOverBox = false;

Serial port;

void setup() {
  size(200, 200);
  boxX = width / 2.0;
  boxY = height / 2.0;
  rectMode(RADIUS);

  println(Serial.list());

  // Open the port that the Arduino board is connected to (in this case #0)
  // Make sure to open the port at the same speed Arduino is using (9600bps)
  port = new Serial(this, Serial.list()[0], 9600);
}

void draw() {
  background(0);

  // Если курсор над квадратом
  if (mouseX > boxX - boxSize && mouseX < boxX + boxSize &&
    mouseY > boxY - boxSize && mouseY < boxY + boxSize) {
    mouseOverBox = true;
    // Рисуем линию вокруг квадрата и меняем его цвет
    stroke(255);
    fill(153);
    // посылаем символ 'H'
    port.write('H');
  } else {
    // возвращаем квадрат в его обычное состояние
    stroke(153);
    fill(153);
    // посылаем символ 'L' для выключения светодиода
    port.write('L');
    mouseOverBox = false;
  }

  // Рисуем квадрат
  rect(boxX, boxY, boxSize, boxSize);
}

04.Communication: Graph (Рисуем график)

Если в предыдущем примере мы посылали данные с компьютера на плату, то теперь выполним обратную задачу - будем получать данные с потенциометра и выводить их в виде графика.

Подсоедините потенциометр стандартным способом. И код для Arduino.


void setup() {
  Serial.begin(9600);
}

void loop() {
  // посылаем значения из вывода A0
  Serial.println(analogRead(A0));
  // небольшая задержка, чтобы стабилизации получения данных
  delay(2);
}

Код для Processing.


import processing.serial.*;

Serial myPort;        // The serial port
int xPos = 1;         // horizontal position of the graph
float inByte = 0;

void setup () {
  // set the window size:
  size(400, 300);

  // List all the available serial ports
  // if using Processing 2.1 or later, use Serial.printArray()
  println(Serial.list());

  // I know that the first port in the serial list on my Mac is always my
  // Arduino, so I open Serial.list()[0].
  // Open whatever port is the one you're using.
  myPort = new Serial(this, Serial.list()[0], 9600);

  // don't generate a serialEvent() unless you get a newline character:
  myPort.bufferUntil('\n');

  // set initial background:
  background(0);
}

void draw () {
  // draw the line:
  stroke(127, 34, 255);
  line(xPos, height, xPos, height - inByte);

  // at the edge of the screen, go back to the beginning:
  if (xPos >= width) {
    xPos = 0;
    background(0);
  } else {
    // increment the horizontal position:
    xPos++;
  }
}

void serialEvent (Serial myPort) {
  // get the ASCII string:
  String inString = myPort.readStringUntil('\n');

  if (inString != null) {
    // trim off any whitespace:
    inString = trim(inString);
    // convert to an int and map to the screen height:
    inByte = float(inString);
    println(inByte);
    inByte = map(inByte, 0, 1023, 0, height);
  }
}

Крутим ручку потенциометра и смотрим за изменениями показаний. Можно заметить, что иногда бывают всплески.

Graph

Управляем цифровыми пинами (черновик)

Как-то набросал небольшой черновик для управления цифровыми пинами через Processing. На экране выводятся 13 квадратов для каждого пина в обратном порядке. Можно включать и выключать питание на выводе щелчком. Соответственно, на плате нужно к выводам присоединить светодиоды. В коде задействованы только два светодиода. Проект не стал дорабатываться, но оставил на память для будущих других проектов.

Листинг для Processing


import processing.serial.*;

color off = color(4, 79, 111);
color on = color(84, 145, 158);

int[] values = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

Serial port;

void setup() {
  size(470, 200);
  //println(Serial.list());
  // Open the port that the Arduino board is connected to (in this case #0)
  // Make sure to open the port at the same speed Arduino is using (9600bps)
  port = new Serial(this, Serial.list()[0], 9600);
}

void draw() {
  background(200);
  stroke(on);

  for (int i = 0; i <= 13; i++) {
    if (values[i] == 1)
      fill(on);
    else
      fill(off);

    rect(420 - i * 30, 30, 20, 20);
  }
}

void mousePressed()
{
  int pin = (450 - mouseX) / 30;

  if (pin == 2 && values[pin] == 0) {
    port.write(53);
    values[pin] = 1;
  } 
  else if(pin == 2 && values[pin] == 1) {
    port.write(33);
    values[pin] = 0;
  }

  if (pin == 12 && values[pin] == 0) {
    port.write(73);
    values[pin] = 1;
  } 
  else if(pin == 12 && values[pin] == 1) {
    port.write(43);
    values[pin] = 0;
  }
}
Processing

Код для Arduino


const int led2 = 2; // вывод 2 для светодиода
const int led13 = 13;
int data;      // переменная для получения данных

void setup() {
  Serial.begin(9600);
  pinMode(led2, OUTPUT);
  pinMode(led13, OUTPUT);
}

void loop() {
  // если есть данные
  if (Serial.available() > 0) {
    // считываем байт в буфере
    data = Serial.read();
    //data = 21;
    //Serial.println(data);

    // если это 53, то включаем светодиод 2
    if (data == 53) {
      digitalWrite(led2, HIGH);
    }
    // если это 33, то выключаем светодиод 2
    if (data == 33) {
      digitalWrite(led2, LOW);
    }

    // если это 63, то включаем светодиод 13
    if (data == 73) {
      digitalWrite(led13, HIGH);
    }
    // если это 43, то выключаем светодиод 13
    if (data == 43) {
      digitalWrite(led13, LOW);
    }
  }
}

Используем кнопки из ControlP5

Спустя некоторое время после создания черновика, наткнулся на библиотеку ControlP5, которая позволяет быстро создать окно с кнопками. Набросал следующий скетч. Пример рассчитан на использование порта COM3.


import controlP5.*;
import processing.serial.*;

Serial port;

ControlP5 cp5;
PFont font;

void setup() {
  size(330, 450);
  surface.setTitle("Управление светодиодами");

  printArray(Serial.list());
  port = new Serial(this, "COM3", 9600);

  cp5 = new ControlP5(this);
  font = createFont("calibri light bold", 20);

  cp5.addButton("red")
    .setPosition(100, 50)
    .setSize(120, 70)
    .setFont(font);

  cp5.addButton("yellow")
    .setPosition(100, 150)
    .setSize(120, 70)
    .setFont(font);

  cp5.addButton("blue")
    .setPosition(100, 250)
    .setSize(120, 70)
    .setFont(font);

  cp5.addButton("alloff")
    .setPosition(100, 350)
    .setSize(110, 70)
    .setFont(font);
}

void draw() {
  background(150, 0, 150);
  fill(0, 255, 0);
  textFont(font);
}

void red() {
  port.write('r');
}

void yellow() {
  port.write('y');
}

void blue() {
  port.write('b');
}

void alloff() {
  // finish
  port.write('f');
}
ControlP5

Соединяем три светодиода с выводами 10, 11, 12. Заливаем скетч на плату, затем запускаем скетч от Processing и нажимаем на кнопки. Первые три кнопки включают отдельные светодиоды, а четвёртая выключает их все сразу.


void setup() {
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);

  Serial.begin(9600);
}

void loop() {
  if (Serial.available()) {
    char val = Serial.read();

    if (val == 'r') {
      digitalWrite(11, HIGH);
    }
    if (val == 'b') {
      digitalWrite(10, HIGH);
    }
    if (val == 'y') {
      digitalWrite(12, HIGH);
    }
    if (val == 'f') {
      digitalWrite(10, LOW);
      digitalWrite(11, LOW);
      digitalWrite(12, LOW);
    }
  }
}

Дополнительное чтение

Джойстик

Meter - библиотека для рисования шкалы

Реклама