Arduino и Visual Basic. Часть 2: Приём данных от Arduino

Примечание

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

Это продолжение статьи Arduino и Visual Basic. Часть 1: Приём данных от Arduino.

В части 1 данные отправлялись с Arduino в приложение Visual Basic и отображались в текстовом поле. Это было хорошо, но приложение не имело представления, что это за данные — оно просто принимало их и отображало.

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

Управляющие коды / Формат пакетов данных

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

Я использую следующий формат: <идентификатор + данные>

  • < — начальный маркер

  • > — конечный маркер

  • Идентификатор — один символ, обозначающий устройство

  • Данные — значение фиксированной длины

Для кнопки: <BH> (нажата / HIGH) и <BL> (отпущена / LOW)

Для потенциометра: <P0000><P1023>

Хотя в Visual Basic можно обрабатывать данные переменной длины, это усложняет жизнь без необходимости. Мне проще использовать форматы фиксированной длины. В данном случае «0000» — «1023».

Данные фиксированной длины значительно проще обрабатывать на Arduino, и фиксированный формат данных становится особенно полезным, когда мы начинаем отправлять данные из VB-приложения в Arduino.

Для преобразования значения потенциометра в ASCII-код данных (это массив символов) я использую функцию formatNumber().

formatNumber(arg1, arg2) принимает 2 аргумента:

  • arg1 — значение для преобразования

  • arg2 — количество символов, то есть длина ASCII-строки

formatNumber( newPotVal, 4);

Команда или код создаётся так:

Serial.print("<P");
Serial.print(numberString);
Serial.print(">");

Я не создаю код целиком, а просто отправляю части кода через последовательный порт.

Значение потенциометра преобразуется в 4-символьную ASCII-строку / массив символов:

  • 0 становится «0000»

  • 200 становится «0200»

  • 1023 становится «1023»

Полный пакет данных: <P0000><P1023>

Вы должны были заметить символы < и >. Это начальный и конечный маркеры, которые позволяют принимающему приложению проверить, что полный пакет данных получен. Приложение проверяет наличие < и > перед обработкой данных.

Использование начальных и конечных маркеров создаёт надёжный и довольно простой способ узнать, что получены целые пакеты данных. Однако это означает, что данные не могут содержать символы, используемые в качестве маркеров.

Я использую ASCII, что означает, что числовые значения нужно преобразовывать в текст. То есть значение 1 становится «1». Это упрощает обработку данных, особенно чисел, но означает, что связь медленнее. Если мы отправляем значение 255, как числовое значение это один байт. Как ASCII-строка — три байта: «2» + «5» + «5».

Схема Arduino

Настройте Arduino следующим образом:

  • D2 — светодиод + резистор

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

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

Схема

Макетная плата Принципиальная схема

Скетч Arduino

/*
* Sketch Arduino and Visual Basic Part 2 - Receiving Data From the Arduino
* Send data over serial to Visual Basic
* https://www.martyncurrey.com/arduino-and-visual-basic-part-1-receiving-data-from-the-arduino/
*/

/*
* Pins
* D2 - LED + resistor
* D3 - push button switch
* A4 - potentiometer
*
* It should noted that no data is sent until something changes
* The sketch can be expanded so that an initial value is sent
*/

// When DEGUG is TRUE print a newline characters
const boolean DEBUG = true;

const byte ledPin = 2;
const byte buttonSwitchPin = 3;
const byte potPin = A4;

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

unsigned int oldPotVal = 0;
unsigned int newPotVal = 0;


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


void setup()
{
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);
  pinMode(buttonSwitchPin, INPUT);

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


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

    if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
    {
        if ( newSwitchState1 != oldSwitchState )
        {
            if ( newSwitchState1 == HIGH ) { Serial.print("<BH>"); } else { Serial.print("<BL>"); }
            if (DEBUG) { Serial.println("");  }
            oldSwitchState = newSwitchState1;
        }
    }

    // The potentiometer I am using jitters a bit so I only using changes of 3 or more.
    newPotVal = analogRead(potPin);
    if ( abs(newPotVal-oldPotVal) > 2)
    {
       oldPotVal = newPotVal;
       formatNumber( newPotVal, 4);
       Serial.print("<P");
       Serial.print(numberString);
       Serial.print(">");
       if (DEBUG) { Serial.println("");  }
    }

    if ( newPotVal >= 1000 ) { digitalWrite(ledPin, HIGH);}
    else                     { digitalWrite(ledPin, LOW); }

   delay (250);
 }


void formatNumber( unsigned int number, byte digits)
{
    // Formats a number in to a string and copies it to the global char array numberString
    // Pads the start of the string with '0' characters
    //
    // number = the integer to convert to a string
    // digits = the number of digits to use.

    char tempString[10] = "\0";
    strcpy(numberString, tempString);

    // convert an integer into a acsii string
    itoa (number, tempString, 10);

    // create a string of '0' characters to pad the number
    byte numZeros = digits - strlen(tempString) ;
    if (numZeros > 0)
    {
       for (int i=1; i <= numZeros; i++)    { strcat(numberString,"0");  }
    }
     strcat(numberString,tempString);
}

Подключите Arduino, загрузите скетч и откройте монитор порта. Вы должны увидеть что-то похожее:

Монитор порта

Поверните потенциометр и проверьте, появляются ли новые значения:

Новые значения потенциометра

Нажмите кнопку:

Нажатие кнопки

Если вы получаете похожие результаты — значит скетч и схема работают.

Результаты работы

Поверните потенциометр до упора. Когда значение достигает 1000 или выше, светодиод должен загореться.

Светодиод включён

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

Скетч довольно простой:

  • Он считывает состояние пина кнопки, и если оно изменилось, отправляет ASCII-команду через последовательный порт.

  • Затем считывает значение потенциометра, и если оно изменилось, отправляет команду потенциометра.

Проверка состояния кнопки. С помощью newSwitchState1 и oldSwitchState можно отслеживать, когда состояние переключателя изменилось.

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

 if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
 {
     if ( newSwitchState1 != oldSwitchState )
     {
         if ( newSwitchState1 == HIGH ) { Serial.print("<BH>"); } else { Serial.print("<BL>"); }
         if (DEBUG) { Serial.println("");  }
         oldSwitchState = newSwitchState1;
     }
 }

С помощью analogRead() считывается значение потенциометра. Если значение изменилось, оно преобразуется в ASCII-команду (через функцию formatNumber()) и отправляется через последовательный порт.

Бонусный код: если значение потенциометра >= 1000, светодиод включается.

 // The potentiometer I am using jitters a bit so I only using changes of 3 or more.
 newPotVal = analogRead(potPin);
 if ( abs(newPotVal-oldPotVal) > 2)
 {
    oldPotVal = newPotVal;
    formatNumber( newPotVal, 4);
    Serial.print("<P");
    Serial.print(numberString);
    Serial.print(">");
    if (DEBUG) { Serial.println("");  }
 }

 if ( newPotVal >= 1000 ) { digitalWrite(ledPin, HIGH);}
 else                     { digitalWrite(ledPin, LOW); }

delay (250);

formatNumber() принимает числовое значение и преобразует его в ASCII-строку фиксированной длины. Функция принимает 2 аргумента: значение для преобразования и количество символов в строке. formatNumber() копирует новую ASCII-строку в глобальный массив символов numberString.

Значение потенциометра преобразуется в 4-символьную ASCII-строку:

  • 0 становится «0000»

  • 200 становится «0200»

  • 1023 становится «1023»

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

    // convert an integer into a acsii string
    itoa (number, tempString, 10);

    // create a string of '0' characters to pad the number
    byte numZeros = digits - strlen(tempString) ;
    if (numZeros > 0)
    {
       for (int i=1; i <= numZeros; i++)    { strcat(numberString,"0");  }
    }

    strcat(numberString,tempString);

}

Примечание:

formatNumber() не проверяет размер буфера numberString.

itoa не проверяет размер буфера numberString.

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

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

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

Также добавлены дополнительные метки для отображения новых полученных значений и пара элементов для понимания работы приложения: TIMER SPEED и CODE COUNT. Надеюсь, их назначение очевидно.

К форме добавлено несколько элементов:

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

RECEIVED DATA переименована в DEBUG DATA

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

BUTTON SWITCH

Показывает состояние кнопки, подключённой к Arduino.

POTENTIOMETER

Показывает значение потенциометра, подключённого к Arduino.

CODE COUNT

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

Сбрасывается в 0 при очистке текстового поля.

Тестирование приложения

Если вам не терпится увидеть новое приложение в действии, запустите Arduino, запустите Visual Studio, загрузите файлы проекта и нажмите маленькую кнопку Start.

Дождитесь запуска приложения, выберите COM PORT и нажмите CONNECT.

Приложение запущено

Когда данные получены, они копируются в текстовое поле. Если данные являются валидным кодом, обновляется поле Button Switch или поле потенциометра.

Получение данных Данные кнопки и потенциометра

В качестве бонуса, когда значение потенциометра 100 или выше, значение потенциометра отображается красным цветом. Захватывающе!

Потенциометр красный

Программа Visual Basic

Вот код. Вы можете скачать его ниже.

' Arduino and Visual Basic Part 2: Receiving Data From An Arduino
' A simple example of recieving serial data from an Arduino and displaying it in a text box
' https://www.martyncurrey.com/arduino-and-visual-basic-part-1-receiving-data-from-the-arduino/
'

Imports System
Imports System.IO.Ports
Imports System.Windows.Forms.VisualStyles.VisualStyleElement

Public Class Form1

    ' Global variables.
    ' Anything defined here is available in the whole app
    Dim selected_COM_PORT As String
    Dim receivedData As String = ""
    Dim commandCount As Integer = 0

    ' This called when the app first starts. Used to initial what ever needs initialising.
    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
    End Sub

    ' When the value of the COM PORT drop doewn list changes,
    ' copy the new value to the comPORT variable.
    Private Sub comPort_ComboBox_SelectedIndexChanged(sender As Object, e As EventArgs) Handles comPort_ComboBox.SelectedIndexChanged
        If (comPort_ComboBox.SelectedItem <> "") Then
            selected_COM_PORT = comPort_ComboBox.SelectedItem
        End If
    End Sub

    ' Try to open the com port or close the port if already open
    Private Sub connect_BTN_Click(sender As Object, e As EventArgs) Handles connect_BTN.Click
        If (connect_BTN.Text = "CONNECT") Then
            If (selected_COM_PORT <> "") Then
                Try
                    SerialPort1.PortName = selected_COM_PORT
                    SerialPort1.BaudRate = 9600
                    SerialPort1.DataBits = 8
                    SerialPort1.Parity = Parity.None
                    SerialPort1.StopBits = StopBits.One
                    SerialPort1.DtrEnable = True
                    SerialPort1.RtsEnable = True
                    SerialPort1.Handshake = Handshake.None
                    SerialPort1.Encoding = System.Text.Encoding.Default 'very important!
                    SerialPort1.ReadTimeout = 10000
                    SerialPort1.Open()
                Catch ex As Exception
                    MessageBox.Show(ex.Message + vbCrLf + "Looks like something else is using it.", "Error opening the serial port", MessageBoxButtons.OK, MessageBoxIcon.Error)
                End Try
            Else
                MsgBox("No COM port selected!")
            End If
        Else
            Try
                SerialPort1.Close()
            Catch ex As Exception
                MessageBox.Show("Serial Port is already closed!")
            End Try
        End If

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

    ' Process received data. Append the data to the text box
    Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        Timer1.Enabled = False
        timer_LBL.Text = "TIMER: OFF"

        receivedData = ReceiveSerialData()

        If (receivedData <> "") Then
            recData_RichTextBox.AppendText("RD = " + receivedData)

            If ((receivedData.Contains("<") And receivedData.Contains(">"))) Then
                parseData()
            End If

            recData_RichTextBox.SelectionStart = recData_RichTextBox.TextLength
            recData_RichTextBox.ScrollToCaret()
        End If

        Timer1.Enabled = True
        timer_LBL.Text = "TIMER: ON"
    End Sub

    ' Check for new data
    Function ReceiveSerialData() As String
        Dim returnData As String = ""
        Dim Incoming As String = ""
        Try
            Incoming = SerialPort1.ReadExisting()
            If Incoming IsNot Nothing Then
                returnData = Incoming
            End If
        Catch ex As TimeoutException
            Return "Error: Serial Port read timed out."
        End Try
        Return returnData
    End Function

    Function parseData()
        Dim pos1 As Integer
        Dim pos2 As Integer
        Dim length As Integer
        Dim newCommand As String
        Dim done As Boolean = False

        While (Not done)
            pos1 = receivedData.IndexOf("<") + 1
            pos2 = receivedData.IndexOf(">") + 1

            If (pos2 < pos1) Then
                receivedData = Microsoft.VisualBasic.Mid(receivedData, pos2 + 1)
                pos1 = receivedData.IndexOf("<") + 1
                pos2 = receivedData.IndexOf(">") + 1
            End If

            If (pos1 = 0 Or pos2 = 0) Then
                done = True
            Else
                length = pos2 - pos1 + 1
                If (length > 0) Then
                    newCommand = Mid(receivedData, pos1 + 1, length - 2)
                    recData_RichTextBox.AppendText("CMD = " & newCommand & vbCrLf)
                    receivedData = Mid(receivedData, pos2 + 1)

                    ' B for button switch
                    If (newCommand(0) = "B") Then
                        If (newCommand(1) = "L") Then
                            buttonSwitchValue_lbl.Text = "LOW"
                        ElseIf (newCommand(1) = "H") Then
                            buttonSwitchValue_lbl.Text = "HIGH"
                        End If
                    End If

                    ' P for potentiometer
                    If (newCommand.Substring(0, 1) = "P") Then
                        Dim tempVal As Integer = Val(newCommand.Substring(1, 4))
                        If (tempVal > 999) Then
                            potentiometerValue_lbl.ForeColor = Color.Red
                        Else
                            potentiometerValue_lbl.ForeColor = Color.Black
                        End If
                        potentiometerValue_lbl.Text = tempVal
                    End If

                    commandCount = commandCount + 1
                    commandCountVal_lbl.Text = commandCount
                End If
            End If
        End While
    End Function

    ' Clear the text box
    Private Sub clear_BTN_Click(sender As Object, e As EventArgs) Handles clear_BTN.Click
        recData_RichTextBox.Text = ""
        commandCount = 0
        commandCountVal_lbl.Text = commandCount
    End Sub

End Class

Подробный разбор программы

Код начинается с объявления 3 глобальных переменных. COM_PORT и receivedData — те же, что и раньше. commandCount — новая.

COM_PORT хранит COM-порт после выбора пользователем из выпадающего списка.

receivedData хранит данные, полученные через последовательный канал.

commandCount хранит количество полученных пакетов данных. В идеале она должна называться dataPacketCount, но ладно.

' Global variables.
Dim selected_COM_PORT As String
Dim receivedData As String = ""
Dim commandCount As Integer = 0

Далее идёт Form1_Load(). Вызывается один раз при первом запуске приложения. Как и в предыдущей части, используется для инициализации — получения списка доступных COM-портов и загрузки списка в comboBox.

TimerSpeed_value_lbl обновляется частотой срабатывания таймера.

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
End Sub

Когда нажимается кнопка CONNECT, вызывается connect_BTN_Click. Здесь открывается последовательный порт. Это подробно описано в предыдущем руководстве, поэтому не буду повторяться.

Функция Timer1_Tick расширена. Теперь добавлена проверка на наличие символов < и > — начального и конечного маркеров. Если они присутствуют, значит данные содержат полный код или команду, и вызывается функция parseData(). parseData() использует глобальную переменную receivedData.

Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick

    'stop the timer
    Timer1.Enabled = False
        timer_LBL.Text = "TIMER: OFF"

        receivedData = ReceiveSerialData()

    If (receivedData <> "") Then
        recData_RichTextBox.AppendText("RD = " + receivedData)

        If ((receivedData.Contains("<") And receivedData.Contains(">"))) Then
            parseData()
        End If

        recData_RichTextBox.SelectionStart = recData_RichTextBox.TextLength
        recData_RichTextBox.ScrollToCaret()
    End If

    Timer1.Enabled = True
    timer_LBL.Text = "TIMER: ON"
End Sub

Функция parseData() — новая. Она берёт переменную receivedData и ищет код/команду. Если находит — проверяет, что это, и действует соответственно.

Фактический код/команда без начальных и конечных маркеров копируется из receivedData. Для этого определяются позиции маркеров, а данные между ними копируются в переменную newCommand.

newCommand проверяется на ожидаемые значения: B — для кнопки, P — для потенциометра.

Вместо поиска полной команды (что можно было бы сделать так же легко) я проверяю первый символ команды:

If (newCommand(0) = "B") Then

Если команда начинается с B, я знаю, что она для кнопки, и следующий символ должен быть H или L:

If (newCommand(1) = "L") Then
     buttonSwitchValue_lbl.Text = "LOW"
 ElseIf (newCommand(1) = "H") Then
     buttonSwitchValue_lbl.Text = "HIGH"
 End If

Для этого простого примера нет необходимости в двухэтапной логике, и простая проверка на «BL» и «BH» была бы проще:

If (newCommand= "BL") Then           buttonSwitchValue_lbl.Text = "LOW"
ElseIf (newCommand= "BH") Then   buttonSwitchValue_lbl.Text = "HIGH"
End If

Для потенциометра я проверяю P. В отличие от кнопки, которая имеет только два состояния, потенциометр имеет значение, которое нужно извлечь. Для этого используется переменная tempVal.

Оператор val() преобразует ASCII-строку в числовое значение. Это противоположность тому, что происходит на Arduino, где числовое значение преобразуется в строку.

newCommand включает начальную P, которая не нравится оператору val(), поэтому я получаю только числовые символы с помощью substring. newCommand.Substring(1, 4) возвращает часть newCommand, начиная с символа в позиции 1 и длиной 4.

If (newCommand.Substring(0, 1) = "P") Then
    Dim tempVal As Integer = Val(newCommand.Substring(1, 4))

Получив фактическое значение, его можно отобразить в форме. Однако если значение > 999, оно отображается красным:

If (tempVal > 999) Then
                        potentiometerValue_lbl.ForeColor = Color.Red
                    Else
                        potentiometerValue_lbl.ForeColor = Color.Black
                    End If
                    potentiometerValue_lbl.Text = tempVal

Вот и всё.

Приложение Visual Basic теперь может обрабатывать и определять управляющие коды и отображать значения в соответствующих полях формы.

Скачать

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

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

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

В части 3 мы рассмотрим отправку данных из VB-приложения в Arduino. Будет интересно!

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