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
Любые данные, не заключённые в начальные и конечные маркеры (< и >), игнорируются приложением.
Красным отображаются внутренние данные.
Поверните потенциометр — светодиод должен стать ярче или тусклее. Новое значение яркости отправляется в приложение, и положение ползунка корректируется.
Теперь либо нажмите кнопку на макетной плате, либо нажмите кнопку APP CONTROL в приложении.
Я использовал кнопку. На макетной плате зелёный светодиод гаснет. В приложении кнопка APP CONTROL становится зелёной.
Вы можете видеть команду <ARD OFF> в текстовом поле DEBUG DATA. Именно эта команда сообщает приложению, что нужно взять управление на себя.
Теперь потенциометр ничего не делает, и светодиодом можно управлять только ползунком в приложении.
Поэкспериментируйте с элементами управления, отключитесь и подключитесь снова, очистите отладочную информацию.
Скетч 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_OFF — turnArduinoControlOn().
Если функция находит 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
Приложение, хотя и не самое большое из когда-либо созданных, значительно больше предыдущего. Помимо кода для обработки новых элементов, добавлена базовая обработка ошибок на последовательном порту.
Код 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 вызывается 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 вызывается 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 для запуска.
Скачать
Скачать скетч 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
Microsoft — Visual Basic Fundamentals for Absolute Beginners
freecodecamp (YouTube) — Learn Visual Basic (.NET) - Full Course
computer Science (YouTube) — Visual Basic.NET Programming (21 lessons)
Derek Banas (YouTube) — Visual Basic Tutorial 2017
tutlane — Visual Basic (VB) Tutorial