Arduino и Visual Basic. Часть 3: Отправка и приём данных между Arduino и Visual Basic

Примечание

Оригинал статьи: martyncurrey.com

Хотя часть 2 была более продвинутой, чем часть 1, она по-прежнему отправляла данные только в одном направлении — от Arduino к приложению Visual Basic, и данные были очень простыми. Пришло время добавить более сложные команды и двустороннюю связь.

Хотя я использовал скетч и приложение Visual Basic из предыдущего руководства как отправную точку, скетч и приложение в этой части значительно сложнее:

  • Команды стали более информативными / сложными

  • Добавлена отправка данных из приложения

  • Добавлен приём данных на Arduino

  • Добавлены некоторые «красивости», например, отключение ползунка приложения, когда он не используется

Схема

Схема та же, что и раньше, за исключением того, что добавлен зелёный светодиод, а красный светодиод и кнопка перемещены.

Макетная плата Принципиальная схема
  • D3 — красный светодиод + резистор

  • D4 — кнопка, подтянутая к земле резистором 10 кОм

  • D5 — зелёный светодиод + резистор

  • A4 — потенциометр

Красный светодиод используется в качестве основного светодиода. Яркость красного светодиода определяется потенциометром или ползунком в приложении (PWM).

Зелёный светодиод используется как индикатор управления:

  • Зелёный светодиод ВКЛ = Arduino управляет

  • Зелёный светодиод ВЫКЛ = VB-приложение управляет

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

Форма приложения Visual Basic

Некоторые элементы новые, другие перемещены.

Форма приложения

COM PORT и кнопка CONNECT

Выберите нужный COM-порт и нажмите connect.

DEBUG DATA — текстовое поле

Показывает внутренние данные, такие как полученные команды. Полученные данные — чёрным, отправленные — синим, всё остальное — красным.

APP CONTROL — кнопка

Переключает, кто управляет светодиодом — Arduino или приложение.

LED BRIGHTNESS — ползунок/трекбар

Когда Arduino управляет — ползунок показывает текущее значение яркости светодиода.

Когда приложение управляет — ползунок устанавливает яркость светодиода.

TIMER — метка

Показывает, когда активен таймер проверки последовательного порта.

TIMER SPEED

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

CODE COUNT

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

CLEAR — кнопка

Очищает текстовое поле DEBUG DATA и сбрасывает CODE COUNT.

Зачем нужна функция управления?

Потенциометр имеет фиксированное физическое положение, и его значение нельзя установить из VB-приложения. Если не отключить/игнорировать потенциометр, пока приложение отправляет новые данные яркости светодиода, потенциометр просто перезапишет их.

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

Давайте попробуем

Настройте Arduino, загрузите скетч.

Скачайте проект Visual Basic, откройте в Visual Studio и нажмите маленькую кнопку Start вверху.

Запуск приложения

Выберите COM-порт и нажмите кнопку CONNECT.

Подключение

Приложение должно подключиться к Arduino и начать принимать команды.

Ползунок LED BRIGHTNESS будет регулироваться, показывая яркость светодиода.

По мере получения команд они отображаются в текстовом поле DEBUG DATA. Всё, что чёрным — данные, полученные приложением.

  • Arduino is ready — стартовое сообщение

  • <ARD ON> — сообщает приложению, что Arduino управляет

  • <LB097> — LB (LED Brightness) — текущее значение PWM (097), поданное на красный светодиод. Значение PWM — это просто значение потенциометра, делённое на 4. 0-1023 / 4 = 0-255

Любые данные, не заключённые в начальные и конечные маркеры (< и >), игнорируются приложением.

Красным отображаются внутренние данные.

Поверните потенциометр — светодиод должен стать ярче или тусклее. Новое значение яркости отправляется в приложение, и положение ползунка корректируется.

LED яркость 97 Приложение LED 0 Макетная плата LED 0 Приложение LED 255 Макетная плата LED 255

Теперь либо нажмите кнопку на макетной плате, либо нажмите кнопку APP CONTROL в приложении.

Я использовал кнопку. На макетной плате зелёный светодиод гаснет. В приложении кнопка APP CONTROL становится зелёной.

Вы можете видеть команду <ARD OFF> в текстовом поле DEBUG DATA. Именно эта команда сообщает приложению, что нужно взять управление на себя.

Зелёный LED выключен, приложение Зелёный LED выключен, макетная плата

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

Приложение, яркость 74 Макетная плата, яркость 74 Приложение, яркость 15 Макетная плата, яркость 15

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

Скетч Arduino

Немного сложнее, чем в предыдущем руководстве. Чтобы сделать основной loop() легче для чтения, основные блоки кода вынесены в отдельные функции.

/*
* Sketch Arduino and Visual Basic Part 3
* Send and receive between Arduino and Visual Basic
* https://www.martyncurrey.com/arduino-and-visual-basic-part-3-receiving-data-from-the-arduino/
*
* potentiometer used to control LED PWM
* push button switch, turn LED on and off
*/

/*
* Pins
* D3 - LED + resistor
* D4 - push button switch
* D5 - control LED + resistor
* A4 - potentiometer
*/

// When DEGUG is TRUE print newline characters
// Useful when using the serial monitor
const boolean DEBUG = false;

const byte redLedPin       = 3;
const byte buttonSwitchPin = 4;
const byte controlLEDPin   = 5;
const byte potPin          = A4;

boolean newSwitchState1 = LOW;
boolean newSwitchState2 = LOW;
boolean newSwitchState3 = LOW;
boolean oldSwitchState  = LOW;

unsigned int oldPotVal = 0;
unsigned int newPotVal = 0;
const int potUpdateFreq = 200;  //  how often the potentiometer is read
long timeNow  = 0;
long timePrev = 0;

// used to hold an ascii representation of a number [10] allows for 9 digits
char numberString[10];

boolean haveNewData = false;
boolean ArduinoHasControl = false;  // false used for off

char recDataBuffer [31];
int length = 30;


void setup()
{
  pinMode(redLedPin, OUTPUT);       digitalWrite(redLedPin ,LOW);
  pinMode(controlLEDPin , OUTPUT);  digitalWrite(controlLEDPin  ,LOW);

  pinMode(buttonSwitchPin, INPUT);

  Serial.begin(115200);
  while (!Serial) {;}
  Serial.println("Adruino is ready");
  Serial.println(" ");

  Serial.print("<ARD_ON>");
  if (DEBUG) { Serial.println(""); }
  turnArduinoControlOn();
}


void loop()
{
  checkSerialIn();
  if (haveNewData)   { processNewData(); }

  checkPushButtonSwitch();

  if (ArduinoHasControl)
  {
      timeNow = millis();
      if (timeNow - timePrev >= potUpdateFreq )
      {
            timePrev = timeNow;
            updatePotentiometer();
      }
  }

}  //  loop()



void turnArduinoControlOn()
{
  ArduinoHasControl = true;
  oldPotVal =0;
  updatePotentiometer();
  digitalWrite(controlLEDPin ,HIGH);
}

void turnArduinoControlOff()
{
  ArduinoHasControl = false;
  digitalWrite(controlLEDPin,LOW);
}

void checkSerialIn()
{
     // checks serial in.
     // if data is available, and if the start marker is received, copies it to a buffer
     // when the end marker is received stop copying and set haveNewData = true
     static boolean recvInProgress = false;
     static byte ndx = 0;
     char startMarker = '<';
     char endMarker = '>';
     char rc;
     if (Serial.available() > 0)
     {
             rc = Serial.read();
             if (recvInProgress == true)
             {
                      if (rc != endMarker)
                      {
                             recDataBuffer[ndx] = rc;
                             ndx++;
                             if (ndx > length) { ndx = length; }
                      }
                      else
                      {
                              recDataBuffer[ndx] = '\0'; // terminate the string
                              recvInProgress = false;
                              ndx = 0;
                              haveNewData = true;
                      }
             }
             else if (rc == startMarker) { recvInProgress = true; }
     }
}

void processNewData()
{

  if      (strcmp(recDataBuffer, "VB_ON")  == 0)  { turnArduinoControlOff();  haveNewData = false; }
  else if (strcmp(recDataBuffer, "VB_OFF") == 0)  { turnArduinoControlOn();   haveNewData = false; }

  // LB = LED Brightness
  else if (recDataBuffer[0] == 'L' && recDataBuffer[1] == 'B' )
  {
      // convert the ascii numbers to an actual number
      int LED_PWN_val = 0;
      LED_PWN_val  =              (recDataBuffer[2] - 48) * 100;
      LED_PWN_val = LED_PWN_val + (recDataBuffer[3] - 48) * 10;
      LED_PWN_val = LED_PWN_val +  recDataBuffer[4] - 48;

      analogWrite(redLedPin,LED_PWN_val);
      haveNewData = false;
  }

     else
     {
             // unknown command
             haveNewData = false;
     }

}

void checkPushButtonSwitch()
{
    // simple debounce
    newSwitchState1 = digitalRead(buttonSwitchPin);      delay(1);
    newSwitchState2 = digitalRead(buttonSwitchPin);      delay(1);
    newSwitchState3 = digitalRead(buttonSwitchPin);

    if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
    {
        if ( newSwitchState1 != oldSwitchState )
        {
            oldSwitchState = newSwitchState1;

           // has the button switch been closed?
           if ( newSwitchState1 == HIGH )
           {
                if ( ArduinoHasControl == false )
                {
                    Serial.print("<ARD_ON>");
                    if (DEBUG) { Serial.println(""); }
                    turnArduinoControlOn();
                }
                else
                {
                  Serial.print("<ARD_OFF>");
                  if (DEBUG) { Serial.println(""); }
                  turnArduinoControlOff();
                }
            }
       }
    }

}  //  checkPushButtonSwitch()

void updatePotentiometer()
{
    newPotVal = analogRead(potPin);
    if ( abs(newPotVal-oldPotVal) > 4)
    {
        oldPotVal = newPotVal;

        int LED_PWN_val = newPotVal / 4;
        analogWrite(redLedPin,LED_PWN_val);

        formatNumber( LED_PWN_val, 3);
        Serial.print("<LB");
        Serial.print(numberString);
        Serial.print(">");
        if (DEBUG) { Serial.println("");  }
    }

}  // updatePotentiometer()

void formatNumber( unsigned int number, byte digits)
{
    char tempString[10] = "\0";
    strcpy(numberString, tempString);

    itoa (number, tempString, 10);

    byte numZeros = digits - strlen(tempString) ;
    if (numZeros > 0)
    {
       for (int i=1; i <= numZeros; i++)    { strcat(numberString,"0");  }
    }
     strcat(numberString,tempString);
} // formatNumber

Подробный разбор скетча

Serial.begin()

Serial.begin(115200);

Вы могли заметить, что скорость увеличена до 115200 бод. Хотя 9600 едва справлялась с односторонней связью от Arduino, этого недостаточно теперь, когда есть ползунок, отправляющий данные. Ползунок в Visual Basic создаёт событие каждый раз, когда ручка перемещается, и каждый раз отправляет новое значение на Arduino. Это может создать большой объём последовательного трафика.

Есть способы смягчить это, например, отправлять новое значение только после того, как пользователь закончит перемещать ручку, но это не создаёт плавного пользовательского опыта.

115200 бод — это нормально и даже рекомендуется при использовании аппаратного последовательного порта. Однако для программного последовательного порта это не работает. Максимальная скорость, которую я успешно использовал с Software Serial — 38400.

Основной loop()

void loop()
{
  checkSerialIn();
  if (haveNewData)   { processNewData(); }

  checkPushButtonSwitch();

  if (ArduinoHasControl)
  {
      timeNow = millis();
      if (timeNow - timePrev >= potUpdateFreq )
      {
            timePrev = timeNow;
            updatePotentiometer();
      }
  }
}
  • checkSerialIn() — проверка входящих данных

  • haveNewData и processNewData() — обработка новых данных

  • checkPushButtonSwitch() — проверка кнопки

  • Если ArduinoHasControl и пришло время — вызов updatePotentiometer()

Теперь Arduino принимает команды от приложения, нужен код для приёма и обработки данных. checkSerialIn() выполняет приём, processNewData() — обработку.

checkSerialIn() основана на recvWithStartEndMarkers() от robin2 с форума Arduino. Она проверяет последовательный порт на новые данные и копирует их в глобальный буфер recDataBuffer. Эта функция особенная, потому что она ищет начальные и конечные маркеры и не начинает копировать данные, пока не найдёт начальный маркер.

Когда checkSerialIn() находит конечный маркер, она устанавливает haveNewData в true.

Если есть новые данные (haveNewData = true), вызывается processNewData().

processNewData() проверяет новые данные на известные команды. В этом примере всего 3 команды:

  • VB_ON — VB-приложение взяло управление

  • VB_OFF — VB-приложение вернуло управление Arduino

  • LBnnn — VB-приложение изменило яркость LED. nnn — число от 0 до 255

При получении VB_ON вызывается turnArduinoControlOff(), а при VB_OFFturnArduinoControlOn().

Если функция находит LB, значит это команда яркости LED, и после символов LB идёт 3-значное число.

Простой способ преобразовать ASCII-представление числа (например, «1») — вычесть 48 из ASCII-значения. ASCII-код «0» — это 48, так что 48-48 = 0. ASCII-код «1» — это 49, так что 49-48 = 1.

void processNewData()
{
  if      (strcmp(recDataBuffer, "VB_ON")  == 0)  { turnArduinoControlOff();  haveNewData = false; }
  else if (strcmp(recDataBuffer, "VB_OFF") == 0)  { turnArduinoControlOn();   haveNewData = false; }

  else if (recDataBuffer[0] == 'L' && recDataBuffer[1] == 'B' )
  {
      int LED_PWN_val = 0;
      LED_PWN_val  =              (recDataBuffer[2] - 48) * 100;
      LED_PWN_val = LED_PWN_val + (recDataBuffer[3] - 48) * 10;
      LED_PWN_val = LED_PWN_val +  recDataBuffer[4] - 48;

      analogWrite(redLedPin,LED_PWN_val);
      haveNewData = false;
  }

     else
     {
             haveNewData = false;
     }
}

checkPushButtonSwitch() проверяет кнопку. На этот раз кнопка работает как переключатель (toggle). Меня больше не интересует, нажата ли кнопка или нет — теперь я хочу знать только, нажата ли она и не была ли нажата до этого, и если нажата — переключить управление.

Если состояние переключателя HIGH, значит оно изменилось, и можно переключить состояние переменной ArduinoHasControl.

Почему не использовать Delay(200), как в частях 1 и 2? Предыдущие скетчи были очень простыми, и короткая задержка ничему не мешала. В этом новом скетче происходит гораздо больше, и я не хотел заставлять скетч ждать без необходимости. delay() блокирует скетч. Используя таймер, скетч продолжает работать и переходит к потенциометру только когда приходит время.

updatePotentiometer() считывает значение потенциометра, проверяет, изменилось ли значение на 5 или более, и если да — обновляет PWM-сигнал на красном светодиоде и отправляет команду LED Brightness (LB).

Почему отправлять только при изменении на 5 или более? Низкое качество чего-то — либо потенциометра, либо макетной платы — вызывает дрожание значения.

void updatePotentiometer()
{
    newPotVal = analogRead(potPin);
    if ( abs(newPotVal-oldPotVal) > 4)
    {
        oldPotVal = newPotVal;

        int LED_PWN_val = newPotVal / 4;
        analogWrite(redLedPin,LED_PWN_val);

        formatNumber( LED_PWN_val, 3);
        Serial.print("<LB");
        Serial.print(numberString);
        Serial.print(">");
        if (DEBUG) { Serial.println("");  }
    }
}

Вспомогательные функции:

turnArduinoControlOn() и turnArduinoControlOff() переключают управление. Когда управление возвращается Arduino, turnArduinoControlOn() также сразу обновляет потенциометр.

void turnArduinoControlOn()
{
  ArduinoHasControl = true;
  oldPotVal =0;
  updatePotentiometer();
  digitalWrite(controlLEDPin ,HIGH);
}

void turnArduinoControlOff()
{
  ArduinoHasControl = false;
  digitalWrite(controlLEDPin,LOW);
}

formatNumber(…) принимает числовое значение (integer) и преобразует его в ASCII-строку. Это функция, которую автор часто использует и просто копирует в новые скетчи.

Приложение Visual Basic

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

Приложение VB

Код Visual Basic достаточно длинный, поэтому не буду занимать место его полным листингом здесь. Для быстрого просмотра нажмите здесь, чтобы загрузить текстовый файл с кодом.

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

Логика VB-приложения сложнее и теперь включает действия, которые могут быть вызваны двумя или более событиями. Например, закрытие последовательного соединения может быть вызвано нажатием пользователем кнопки DIS-CONNECT или ошибкой чтения/записи COM-порта (например, Arduino выключена). Вместо дублирования кода он вынесен в функцию.

Основные части приложения Visual Basic

При первом запуске приложения вызывается Form1_Load(). Новое в части 3:

  • AppControlButtonOff() — отключает кнопку App Control

  • turnAppControllOff() — устанавливает приложение в состояние «выключено»

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Timer1.Enabled = False
    selected_COM_PORT = ""
    For Each sp As String In My.Computer.Ports.SerialPortNames
        comPort_ComboBox.Items.Add(sp)
    Next

    TimerSpeed_value_lbl.Text = Timer1.Interval
    AppControlButtonOff()
    turnAppControllOff()
End Sub

Private Sub MainForm_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
    If (SerialPort1.IsOpen) Then
        Try
            SerialPort1.Close()
        Catch ex As Exception
            MessageBox.Show("Serial Port is already closed!")
        End Try
    End If
End Sub
COM-порт и кнопка Connect

Выбор COM-порта и подключение к последовательному порту

При нажатии кнопки CONNECT вызывается connect_BTN_Click(…). Сначала функция проверяет, подключается пользователь или отключается, по тексту кнопки — CONNECT или DIS-CONNECT.

Если кнопка говорит CONNECT — функция пытается открыть последовательное соединение. Процесс обёрнут в try/catch для перехвата проблем.

Если кнопка говорит DIS-CONNECT — соединение закрывается.

Наконец вызывается checkSerialPortandUpdateScreen(), которая обновляет экран.

Private Sub checkSerialPortandUpdateScreen()
    If (SerialPort1.IsOpen) = True Then
        connect_BTN.Text = "DIS-CONNECT"
        Timer1.Enabled = True
        timer_LBL.Text = "TIMER: ON"
        AppControlButtonOn()
    Else
        connect_BTN.Text = "CONNECT"
        Timer1.Enabled = False
        timer_LBL.Text = "TIMER: OFF"
        AppControlButtonOff()
    End If
End Sub

Приём последовательных данных

Помните таймер, запущенный при нажатии кнопки CONNECT? Он используется для проверки входящих данных. При срабатывании таймера вызывается Timer1_Tick(…).

Первым делом Timer1_Tick(…) останавливает таймер — не хотите, чтобы таймер срабатывал, пока вы внутри функции таймера.

Затем функция проверяет, что порт всё ещё открыт. Если соединение есть — пытается прочитать новые данные через ReceiveSerialData().

После получения новых данных, receivedData проверяется на наличие символов < и >. Если оба присутствуют — вызывается parseData().

parseData()

parseData() вызывается только когда приложение «довольно уверено», что есть что парсить. Однако «довольно уверено» — это не 100%.

Последовательные данные — это серия маленьких данных, а не один большой блок. Когда вы отправляете HELLO, слово не отправляется целиком за раз — отправляются отдельные символы.

Что это значит? Не предполагайте, что у вас есть все данные. То, что вы отправили данные, не означает, что они все прибыли — часть может быть ещё в пути.

Простое решение: убедиться, что начальный маркер идёт перед конечным. Если конечный маркер раньше — удалить его и проверить снова.

Есть только три команды:

  • ARD_OFF — Arduino берёт перерыв, приложение должно взять управление

  • ARD_ON — Arduino вернулась и забирает управление

  • LBnnn — Яркость LED изменена через потенциометр. nnn — 3-значное число

Действия пользователя

Пользователь может:

  • Закрыть приложение

  • Нажать DIS-CONNECT при подключении

  • Нажать кнопку APP CONTROL

  • Переместить ползунок при управлении приложением

Кнопка APP CONTROL

APP CONTROL OFF APP CONTROL ON

При нажатии кнопки APP CONTROL вызывается CONTROL_BTN_Click(…):

  • Вызывает turnAppControllOn() и отправляет <VB_ON> на Arduino, или

  • Вызывает turnAppControllOff() и отправляет <VB_OFF>

Private Sub turnAppControllOn()
    CONTROL_BTN.Text = "APP CONTROL ON"
    CONTROL_BTN.BackColor = Color.Lime
    SLIDER_trackBar.Enabled = True
End Sub

Private Sub turnAppControllOff()
    CONTROL_BTN.Text = "APP CONTROL OFF"
    CONTROL_BTN.BackColor = Color.Tomato
    SLIDER_trackBar.Enabled = False
End Sub

После включения ползунка начинает работать событие изменения ползунка. При каждом изменении значения ползунка вызывается SLIDER_trackBar_Scroll(…), которая отправляет новое значение на Arduino:

Private Sub SLIDER_trackBar_Scroll(sender As Object, e As EventArgs) Handles SLIDER_trackBar.Scroll
    Dim tmpVal As String = Format(SLIDER_trackBar.Value, "000")
    SLIDER_VAL_LBL.Text = tmpVal
    writeSerial("<LB" + tmpVal + ">")
End Sub

Функция debug(…) добавляет данные в текстовое поле DEBUG DATA с цветовой кодировкой:

  • 0 (по умолчанию) — чёрный

  • 1 — синий

  • 2 — красный

Private Sub debug(data As String, Optional col As Integer = 0)
    If (col = 0) Then
        recData_RichTextBox.SelectionColor = Color.Black
    ElseIf (col = 1) Then
        recData_RichTextBox.SelectionColor = Color.Blue
    ElseIf (col = 2) Then
        recData_RichTextBox.SelectionColor = Color.Red
    End If

    recData_RichTextBox.AppendText(data & vbCrLf)
    recData_RichTextBox.SelectionStart = recData_RichTextBox.TextLength
    recData_RichTextBox.ScrollToCaret()
End Sub

Скомпилированный exe-файл

Скомпилированный exe-файл можно найти внутри папки проекта:

Arduino_Visual-Basic_Part3\Arduino_Visual-Basic\Arduino_Visual-Basic\bin\Release

Просто дважды кликните на файл Arduino_Visual-Basic.exe для запуска.

Exe файл

Скачать

Скачать скетч Arduino

Скачать проект Visual Basic. Для использования файлов проекта VB необходимо установить Visual Studio.

Следующие шаги

Поэкспериментируйте со скетчем и приложением Visual Basic и модифицируйте их для других задач.

Если вы хотите узнать больше об отправке сложных команд или пакетов данных, смотрите Arduino Serial: ASCII Data and Using Markers to Separate Data.

Что можно улучшить

Список COM-портов создаётся при первом запуске приложения. Это означает, что Arduino должна быть включена и подключена до запуска приложения. Что произойдёт, если вы откроете приложение до Arduino? COM-порт Arduino не будет в списке.

Простое решение — добавить кнопку обновления списка COM-портов.

Также можно позволить пользователю выбирать свойства порта, например, скорость передачи или настройки чётности. Просто добавьте необходимые выпадающие списки в форму.

Изучение Visual Basic NET