Робота з Arduino з C# додатка


У цій статті я хотів би розповісти про те, як можна зчитувати дані і управляти платою Arduino, підключеного через USB порт, з .Net додатки і програми UWP.

Робити це можна без використання сторонніх бібліотек. Фактично, використовуючи тільки віртуальний COM порт.

Давайте спочатку напишемо скетч для Arduino. Ми будемо надсилати на порт рядок з текстом, що містить в собі значення змінної, яка у нас буде постійно змінюватися в циклі (таким чином імітуючи дані зняті з датчика). Також будемо зчитувати дані з порту і в разі якщо отримаємо текст «1», то включимо вбудований в плату світлодіод. Він розташований на 13-му піне і позначений на платі латинською літерою L. А якщо отримаємо «0», то вимкнемо його.

int i = 0; // змінна для лічильника імітує показання датчика
int led = 13; 

void setup() {
Serial.begin(9600); // встановимо швидкість обміну даними
pinMode(led, OUTPUT); // і режим роботи 13-ого цифрового піна в якості виходу
}
void loop() {
i = i + 1; // щоб ми змогли помітити що дані змінилися
String stringOne = "Info from Arduino ";
stringOne += i; // конкатенація
Serial.println(stringOne); // відправляємо рядок на порт

char incomingChar;

if (Serial.available() > 0)
{
// зчитуємо отримане з порту значення в змінну
incomingChar = Serial.read(); 
// в залежності від значення змінної включаємо або вимикаємо LED
switch (incomingChar) 
{
case '1':
digitalWrite(led, HIGH);
break;
case '0':
digitalWrite(led, LOW);
break;
}
}
delay(300);
}

WPF додаток

Тепер створимо WPF додаток. Розмітку зробимо досить простий. 2 кнопки і мітка для відображення тексту отриманого з порту це все що необхідно:

<StackPanel Orientation="Vertical">
<Label x:Name="lblPortData" FontSize="48" HorizontalAlignment="Center" Margin="0,20,0,0">Немає даних</Label>
<Button x:Name="btnOne" Click="btnOne_Click" Width="100" Height="30" Margin="0,10,0,0">Послати 1</Button>
<Button x:Name="btnZero" Click="btnZero_Click" Width="100" Height="30" Margin="0,10,0,0">Послати 0</Button>
</StackPanel>

Додамо 2 простору імен:

using System.Timers;
using System.IO.Ports;

І в області видимості класу 2 змінні з делегатом:

System.Timers.Timer aTimer;
SerialPort currentPort;
private delegate void updateDelegate(string txt);

Реалізуємо подія Window_Loaded. В ньому ми пройдемося по всіх доступних портів, послухаємо їх і перевіримо не виводиться чи портом повідомлення з текстом «Info from Arduino». Якщо знайдемо порт надсилає таке повідомлення, то значить знайшли порт Arduino. В такому випадку можна встановити його параметри, відкрити порт і запустити таймер.

bool ArduinoPortFound = false;

try
{
string[] ports = SerialPort.GetPortNames();
foreach (string port in ports)
{
currentPort = new SerialPort(port, 9600);
if (ArduinoDetected())
{
ArduinoPortFound = true;
break;
}
else
{
ArduinoPortFound = false;
}
}
}
catch { }

if (ArduinoPortFound == false) return;
System.Threading.Thread.Sleep(500); // трохи почекаємо

currentPort.BaudRate = 9600;
currentPort.DtrEnable = true;
currentPort.ReadTimeout= 1000;
try
{
currentPort.Open();
}
catch { }

aTimer = new System.Timers.Timer(1000);
aTimer.Elapsed += OnTimedEvent;
aTimer.AutoReset = true;
aTimer.Enabled = true;

Для зняття даних з порту і порівняння їх із заданими я використовував функцію ArduinoDetected:

private bool ArduinoDetected()
{
try
{
currentPort.Open();
System.Threading.Thread.Sleep(1000); 
// невелика пауза, адже SerialPort не терпить суєти

string returnMessage = currentPort.ReadLine();
currentPort.Close();

// необхідно щоб void loop() у скетчі містив код Serial.println("Info from Arduino");
if (returnMessage.Contains("Info from Arduino"))
{
return true;
}
else
{
return false;
}
}
catch (Exception e)
{
return false;
}
}

Тепер залишилося реалізувати обробку події таймера. Метод OnTimedEvent можна згенерувати засобами Intellisense. Його вміст буде таким:

private void OnTimedEvent(object sender, ElapsedEventArgs e)
{
if (!currentPort.IsOpen) return;
try // так як після закриття вікна таймер ще може виконається або межа очікування може бути перевищений
{
// видалимо накопичилася в буфері
currentPort.DiscardInBuffer(); 
// вважаємо останнє значення 
string strFromPort = currentPort.ReadLine(); 
lblPortData.Dispatcher.BeginInvoke(new updateDelegate(updateTextBox), strFromPort);
}
catch { }
}

private void updateTextBox(string txt)
{
lblPortData.Content = txt;
}

Ми зчитуємо значення з порту і виводимо його у вигляді тексту підпису. Але так як таймер у нас працює в потоці відмінному від потоку UI, то нам необхідно використовувати Dispatcher.BeginInvoke. Ось тут нам і згодився оголошений на початку коду делегат.

Після закінчення роботи з портом дуже бажано його закрити. Але так як ми працюємо з ним постійно, поки додаток відкрито, то закрити його логічно при закритті програми. Додамо в наше вікно обробку події Closing:

private void Window_Closing(object sender, EventArgs e)
{
aTimer.Enabled = false;
currentPort.Close();
}

Готове. Тепер залишилося зробити відправку на порт повідомлення з текстом «1» або «0» залежно від натискання кнопки і можна тестувати роботу додатка. Це просто:

private void btnOne_Click(object sender, RoutedEventArgs e)
{
if (!currentPort.IsOpen) return;
currentPort.Write("1");
}

private void btnZero_Click(object sender, RoutedEventArgs e)
{
if (!currentPort.IsOpen) return;
currentPort.Write("0");
}

Одержаний приклад доступний на GitHub.

До речі, WinForms додаток створюється ще швидше і простіше. Достатньо перетягнути на форму з панелі інструментів елемент SerialPort (при наявності бажання можна перетягнути з панелі інструментів і елемент Timer). Після чого в потрібному місці коду, можна відкрити порт, зчитувати з нього дані і писати в нього приблизно як і в WPF додатку.

UWP додаток

Для того щоб розібратися з тим, як працювати з послідовним портом я розглянув наступний приклад: SerialSample
Для дозволу роботи з COM портом в маніфесті програми має бути ось таке оголошення:

<Capabilities>
<Capability Name="internetClient" />
<DeviceCapability Name="serialcommunication">
<Device Id="any">
<Function Type="name:serialPort"/>
</Device>
</DeviceCapability>
</Capabilities>

В коді C# нам знадобляться 4 простору імен:

using Windows.Devices.SerialCommunication;
Windows using.Devices.Enumeration;
Windows using.Storage.Streams;
using System.Threading.Tasks;

І одна змінна в області видимості класу:

string deviceId;

При завантаженні вважаємо в неї значення id порту, до якого підключена плата Arduino:

private async void Page_Loaded(object sender, RoutedEventArgs e)
{
string filt = SerialDevice.GetDeviceSelector("COM3");
DeviceInformationCollection devices = await DeviceInformation.FindAllAsync(filt);

if (devices.Any())
{
deviceId = devices.First().Id;
}
}

Наступний Task вважає 64 байта з порту і відобразить текст в поле з ім'ям txtPortData

private async Task Listen()
{
using (SerialDevice serialPort = await SerialDevice.FromIdAsync(deviceId))
{
if (serialPort != null)
{
serialPort.ReadTimeout = TimeSpan.FromMilliseconds(1000);
serialPort.BaudRate = 9600;
serialPort.Parity = SerialParity.None;
serialPort.StopBits = SerialStopBitCount.One;
serialPort.DataBits = 8;
serialPort.Handshake = SerialHandshake.None;

try
{
using (DataReader dataReaderObject = new DataReader(serialPort.InputStream))
{
Task<UInt32> loadAsyncTask;
uint ReadBufferLength = 64;
dataReaderObject.InputStreamOptions = InputStreamOptions.Partial;
loadAsyncTask = dataReaderObject.LoadAsync(ReadBufferLength).AsTask();
UInt32 bytesRead = await loadAsyncTask; 
if (bytesRead > 0)
{
txtPortData.Text = dataReaderObject.ReadString(bytesRead);
txtStatus.Text = "Read operation completed";
}
}
}
catch (Exception ex)
{
txtStatus.Text = ex.Message;
}
}
}
}

У UWP додатках на C# відсутній метод SerialPort.DiscardInBuffer. Тому один з варіантів, це зчитувати дані відкриваючи кожен раз порт наново, що й було продемонстровано в даному випадку. Якщо ви спробуєте, то зможете помітити, що відлік кожен раз йде з одиниці. Приблизно те ж саме відбувається і в Arduino IDE при відкритті Serial Monitor. Варіант, звичайно, так собі. Відкривати кожен раз порт це не справа, але якщо дані необхідно зчитувати рідко, то цей спосіб зійде. Крім того, таким чином записаний приклад виглядає коротше і зрозуміліше.

Рекомендований варіант це не оголошувати кожен раз порт заново, а оголосити його один раз, наприклад, при завантаженні. Але в такому випадку необхідно буде регулярно зчитувати дані з порту, щоб він не заповнювався мотлохом і дані виявлялися актуальними. Дивіться як це зроблено в моєму прикладі UWP програми. Я так вважаю, що концепт відсутності можливості очистити буфер полягає в тому, що постійно асинхронно знімаються дані, не надто навантажують систему. Як тільки необхідну кількість байт зчитується до буфера, виконується слідом написаний код. Є плюс в тому, що при такому постійному моніторингу нічого не пропустиш, але деяким (і мені в тому числі) не вистачає звичної можливості один раз «считнуть» дані.

Для запису даних в порт можна використовувати схожий код:

private async Task sendToPort(string sometext)
{

using (SerialDevice serialPort = await SerialDevice.FromIdAsync(deviceId))
{
Task.Delay(1000).Wait(); 

if ((serialPort != null) && (sometext.Length != 0))
{
serialPort.WriteTimeout = TimeSpan.FromMilliseconds(1000);
serialPort.ReadTimeout = TimeSpan.FromMilliseconds(1000);
serialPort.BaudRate = 9600;
serialPort.Parity = SerialParity.None;
serialPort.StopBits = SerialStopBitCount.One;
serialPort.DataBits = 8;
serialPort.Handshake = SerialHandshake.None;

Task.Delay(1000).Wait();

try
{

using (DataWriter dataWriteObject = new DataWriter(serialPort.OutputStream))
{

Task<UInt32> storeAsyncTask;

dataWriteObject.WriteString(sometext);

storeAsyncTask = dataWriteObject.StoreAsync().AsTask();

UInt32 bytesWritten = await storeAsyncTask;

if (bytesWritten > 0)
{
txtStatus.Text = bytesWritten + " bytes written";
}
}
}
catch (Exception ex)
{
txtStatus.Text = ex.Message;
}
}
}
}

Ви можете помітити, що після ініціалізації порту і установки параметрів додані паузи по 1 секунді. Без цих пауз змусити Arduino зреагувати не вийшло. Схоже, що serial port дійсно не терпить суєти. Знову ж таки, нагадую, що краще відкрити порт один раз, а не відкривати/закривати його постійно. У такому випадку ніякі паузи не потрібні.
Спрощений варіант UWP додатка, який аналогічний розглянутому вище WPF .Net додатком доступний на GitHub

В результаті я дійшов висновку, що робота в UWP з Arduino безпосередньо через віртуальний COM порт хоч і незвична, але цілком можлива і без підключення сторонніх бібліотек.
Зауважу, що Microsoft досить тісно співпрацює з Arduino, тому різних бібліотек і технологій комунікації, що спрощують розробку, безліч. Найпопулярніша, це звичайно ж працює з протоколом Firmata бібліотека Windows Remote Arduino.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.