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.