C#5.0とWindows.Formsを使ってUIからUSB接続のArduinoと通信する
前書き
正月休みに時間があったので、C#5.0を使ってWindows Formアプリを書きました。ネタとしてArduinoとの通信をやってみたかったので、表題の実装をしました。せっかくなので、紹介します。
一応、Copilotにコードレビューをしてもらってはいるが、初めてC#を触るところから正月休みの内の3~4日間くらいでネットで勉強して作ったので、不足してるところはあると思う。気になるところがあれば、指摘していただけると幸い。
C#とは
C#はC++とJavaの影響を受けてMicrosoftが開発したプログラミング言語。
C#5.0は2012年8月にMicrosoftがリリースしたバージョン。最近の文法や機能が結構使えない。
C#採用の理由
Windows 11/10にコンパイラが標準搭載されており、Windows上であればメモ帳と組み合わせて開発から実行までできてしまうから。(なお、今回はテキストエディタとしてVisual Studio Codeを使用しました。)
仕事ではよくVBA・JavaScript・PHP・C++あたりを使っているが、次のようなデメリットがある。
VBA
- 実行速度が遅い (高速化の工夫をしたとしても。)
- 実行環境を選んでしまう
JavaScript
- ハードウェアやファイルを扱うようなコードが書けない(はず)
PHP
- 実行環境を選んでしまう
C++
- ちょっとしたツールを作るには手間がかかりすぎる
- ちゃんと作らないとバグを生みやすい
(端的にC++が嫌い)
この辺りを背景に、WindowsマシンならC#で場所を選ばず開発でき、それなりの速度で動作することが期待できるため、触っておこうと思った次第。
仕様
外部仕様
- WindowsアプリからArduinoへ接続・切断
- LEDオンボタンとLEDオフボタンを実装
- ArduinoオンボードのLED(DIO13番)をWindowsアプリの操作によって点灯/消灯する
- Arduinoからステータスを受け取り、WindowsアプリのUIを書き換える(内部のステータスではなく、Arduinoから受け取る)
コマンド仕様
- ASCII
- バックスペース(\b)で受信バッファクリア
- ラインフィード(\n)でメッセージ終端(コマンドの確定または応答の終端)
- アプリ->Arduino:"0"でLED消灯し、"1"でLED点灯する
- Arduino->アプリ:"0"でLED消灯状態、"1"でLED点灯状態を示す
- Arduino->アプリは、100ms毎に行う
今回はオンオフ情報だけ通信できればいいので非常に簡単なコマンドだが、このコード自体にはメッセージの区切りも実装していあるので、長さのあるコマンドも対応可能。stringからintなどへの変換やメッセージ内のデータ区切りなどを実装すれば、もっと複雑なコマンド操作とステータス受信ができる。
アプリ内部仕様
- Arduinoとの通信は送信受信共に専用のスレッドを用意し、かつスレッドセーフに実装する
- 送信はキューイングし、スレッドセーフに実装する
- 実行速度や効率よりもコードの見やすさを重視する
Arduino内部仕様
- Delayなど、loop()を止めるような実装はしない
- 実行速度や効率よりもコードの見やすさを重視する
解説
送信をスレッドセーフなキューイングとしたのは、UI以外のスレッドから通信を行えるようにするため。別途タイマースレッドを用意して、何らかの条件によって点滅させるみたいなこともできる。(今回はこのような使い方をしないが、大きなアプリを作るとなると非常に便利)
送信をキューイングするなら、その結果を呼び出し元に返したいところだが、それを動作させるコードまで書くとコード量が多くなりすぎるので、今回は割愛。たぶん、デリゲートと組み合わせて構造体を作れば出来そうな気がする。知らんけど。
Arduinoで一定時間ごとに処理をしたい場合にDelay()を使っている例をよく見かける。分かりやすいのはそうだが、これはあまり賢くない。なぜなら、Delayしている間はArduinoの処理が止まってしまうから。Delayを使わずに定期的に処理を実行させるようにして、Arduinoマイコンで複数の処理を実行させることができるようにする。(今回で言うと、コマンド受付とLEDの制御、アプリへのデータ送信がそれぞれのタイミングで行えるようになっている)
実装
Arduino (C++)
コンパイルと書き込みは詳しく解説してる資料を参考にしてほしい。
bool state = false;
unsigned long lastSerial = 0;
String cmd = "";
const int LED_PIN = 13;
const int SERIAL_INTERVAL = 100;
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
changeLed(false);
}
void loop() {
recieveCmd();
sendStatus();
}
void changeLed(bool light) {
digitalWrite(LED_PIN, light ? HIGH : LOW);
state = light;
}
void recieveCmd() {
char key;
if (Serial.available()) {
key = Serial.read();
if ('\b' == key) {
cmd = "";
} else if ('\n' == key) {
if (cmd.equals("0")) {
changeLed(false);
} else {
changeLed(true);
}
cmd = "";
} else {
cmd += key;
}
}
}
void sendStatus() {
if (SERIAL_INTERVAL < (millis() - lastSerial)) {
lastSerial = millis();
Serial.print(state ? "1\n" : "0\n");
}
}
Windowsアプリケーション(C#)
まずは、コンパイルのためのバッチファイル
build.cmd
@echo off
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /nologo /target:winexe *.cs
こっちが本体。
main.cs
using System;
using System.Windows.Forms;
using System.Drawing;
using System.IO.Ports;
using System.Threading;
using System.Collections.Generic;
namespace WindowsFormsApp
{
internal static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
public partial class Form1 : Form
{
private Button _updateListBtn = new Button();
private ComboBox _portComboBox = new ComboBox();
private Button _connectArduinoBtn = new Button();
private Button _disconnectArduinoBtn = new Button();
private Button _ledOnBtn = new Button();
private Button _ledOffBtn = new Button();
private ArduinoCtrl _arduino;
public Form1()
{
_arduino = new ArduinoCtrl(this);
InitializeComponent();
this.FormClosing += Form1_FormClosing;
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
_updateListBtn.Dispose();
_portComboBox.Dispose();
_connectArduinoBtn.Dispose();
_disconnectArduinoBtn.Dispose();
_ledOnBtn.Dispose();
_ledOffBtn.Dispose();
_arduino.Dispose();
}
private void InitializeComponent()
{
this.Controls.Add(_portComboBox);
this.Controls.Add(_updateListBtn);
this.Controls.Add(_connectArduinoBtn);
this.Controls.Add(_disconnectArduinoBtn);
this.Controls.Add(_ledOnBtn);
this.Controls.Add(_ledOffBtn);
this.Text = "Main window";
this.Size = new Size(500, 200);
_portComboBox.Location = new Point(0, 0);
_portComboBox.Size = new Size(100, 20);
InitializeButton(_updateListBtn, "Reload", new Point(100, 0), new Size(100, 20), UpdatePortListBtn_Click);
InitializeButton(_connectArduinoBtn, "Connect", new Point(200, 0), new Size(100, 20), ConnectBtn_Click);
InitializeButton(_disconnectArduinoBtn, "Disconnect", new Point(300, 0), new Size(100, 20), DisconnectBtn_Click);
InitializeButton(_ledOnBtn, "LED ON", new Point(0, 20), new Size(100, 20), LedOnBtn_Click);
InitializeButton(_ledOffBtn, "LED OFF", new Point(100, 20), new Size(100, 20), LedOffBtn_Click);
ChangeConnectState(false);
UpdateComList();
}
private void InitializeButton(Button button, string text, Point location, Size size, EventHandler clickEvent)
{
button.Text = text;
button.Location = location;
button.Size = size;
button.Click += clickEvent;
}
private void ChangeConnectState(bool state)
{
_portComboBox.Enabled = !state;
_updateListBtn.Enabled = !state;
_connectArduinoBtn.Enabled = !state;
_disconnectArduinoBtn.Enabled = state;
_ledOnBtn.Enabled = state;
_ledOffBtn.Enabled = state;
}
public void UpdateLedState(bool state)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new Action(() => UpdateLedStateMethod(state)));
}
else
{
UpdateLedStateMethod(state);
}
}
private void UpdateLedStateMethod(bool state)
{
_ledOnBtn.Enabled = !state;
_ledOffBtn.Enabled = state;
}
private void UpdatePortListBtn_Click(object sender, EventArgs e)
{
UpdateComList();
}
private void ConnectBtn_Click(object sender, EventArgs e)
{
try
{
_arduino.ConnectArduino(_portComboBox.Text);
ChangeConnectState(true);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void DisconnectBtn_Click(object sender, EventArgs e)
{
try
{
_arduino.DisconnectArduino();
ChangeConnectState(false);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void LedOnBtn_Click(object sender, EventArgs e)
{
_arduino.led(true);
}
private void LedOffBtn_Click(object sender, EventArgs e)
{
_arduino.led(false);
}
private void UpdateComList()
{
string[] ports = ArduinoCtrl.GetPortNames();
_portComboBox.Items.Clear();
foreach (string port in ports)
{
_portComboBox.Items.Add(port);
}
if (_portComboBox.Items.Count > 0)
{
_portComboBox.SelectedIndex = 0;
}
}
}
public class ArduinoCtrl : IDisposable
{
private Form1 _form1;
private SerialPort _serialPort = new SerialPort();
private System.Threading.Timer _sendThread;
private Queue<string> _queue = new Queue<string>();
private string _receivedString = String.Empty;
private readonly object _received_str_lock = new object();
private readonly object _queue_lock = new object();
public ArduinoCtrl(Form1 form1)
{
_form1 = form1;
_sendThread = new System.Threading.Timer(SendProcess, null, Timeout.Infinite, Timeout.Infinite);
}
~ArduinoCtrl()
{
Dispose();
}
static public string[] GetPortNames()
{
return SerialPort.GetPortNames();
}
public void Dispose()
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
_serialPort.Dispose();
}
public void ConnectArduino(string port)
{
if (!_serialPort.IsOpen)
{
_serialPort.BaudRate = 9600;
_serialPort.Parity = Parity.None;
_serialPort.DataBits = 8;
_serialPort.StopBits = StopBits.One;
_serialPort.Handshake = Handshake.None;
_serialPort.PortName = port;
_serialPort.DataReceived += new SerialDataReceivedEventHandler(this.DataReceivedHandler);
_serialPort.Open();
StartTimer();
}
}
public void DisconnectArduino()
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
StopTimer();
}
}
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
lock (_received_str_lock)
{
SerialPort sp = (SerialPort)sender;
string indata = sp.ReadExisting();
foreach (char s in indata)
{
if ('\b' == s)
{
_receivedString = "";
}
if ('\n' == s)
{
_form1.UpdateLedState(_receivedString != "0");
_receivedString = "";
}
else
{
_receivedString += s;
}
}
}
}
public void StartTimer()
{
_sendThread.Change(1000, 100); // 1秒後、250ms間隔
}
public void StopTimer()
{
_sendThread.Change(Timeout.Infinite, Timeout.Infinite);
}
private void SendProcess(object state)
{
if (_serialPort.IsOpen)
{
lock (_queue_lock)
{
if (0 < _queue.Count)
{
string send = _queue.Peek();
_serialPort.Write("\b" + send + "\n");
_queue.Dequeue();
}
}
}
}
public void led(bool light)
{
if (_serialPort.IsOpen)
{
lock (_queue_lock)
{
if (light)
{
_queue.Enqueue("1");
}
else
{
_queue.Enqueue("0");
}
}
}
}
}
}
build.cmdとmain.csを同じフォルダにおいて、build.cmdを実行すると、main.exeが生成される。
感想
命名規則のブレは許してほしい………💦
C#はC++に比べれば非常に簡単に書ける。Widnows.Formsを使えば、UIもそれなりに簡単に書ける。ただ、PHP+htmlによるUIにはちょっと劣るかな、とも思いつつ、それよりは可読性が高く書けるのも感じた。
ちょっとした社内ツールで従来よりも実行速度を重視したものを作りたいときは有効な選択肢だと思った。
ただ、C#5.0はやはり古さを感じて、使い方によっては実装の回りくどさを感じてしまう。また、新しいC#向けの解説サイトも多く、そのコードが使えないことも多い。そうなってくると新しいC#で書きたくなるが、開発環境の構築が必要。結局開発環境構築の手間をかけるなら、C#以外も選択肢かな、とは思う。
Arduinoが標準ではUSB接続にしか対応していないので、PCとの通信が無いとデータの記録収集等がやりづらい。BluetoothやTCP/IPで通信できれば、例えばAndroid端末から無線で操作できたりして、小型省電力といったメリットを生かした何かが作れそう。
コメントを残す