C# : ArduinoとUSB通信する方法

# 先人の足跡：Serial通信
Serialの接続法（データ受信イベントをどうつけるかが分からなくてできなかった→今回はサブスレッドで常時読み続ける方法をとる）

https://qiita.com/mag2/items/d15bc3c9d66ce0c8f6b1

昔のやり方（NET6ではツールボックス「SerialPort」が削除されているため、同じことはできないはず）

https://qiita.com/SAM_tak/items/48b89e7e19abf121071b

### 引っかかり1 : ツールボックスに「SerialPort」がない

https://magnolia-tech.jp/net6_use_serialport_component/

NET.6より削除されて、自分で追加する必要があるようだ

### 引っかかり2 : 受信できない・・・

https://hananekolog.blogspot.com/2014/05/c-arduino.html

イベント関数のみでは動かない！
イベント登録が必要みたいだ

```C
serialPort1.DataReceived += serialPort1_DataReceived;
```
これをどこにいれたらいいのか？・・・

魔法は自由であるべきだ

# 公式のSerial通信方法

https://learn.microsoft.com/ja-jp/dotnet/api/system.io.ports.serialport?view=dotnet-plat-ext-8.0

公式だと別スレッドを立てて常時読み出しを実行している。
別スレッドからコントロール（TextBoxなど）へのデータの引き渡しで困った・・・

Invokeを使用するといいらしい

### namespaceのインストール
右ウィンドウの「ソリューションエクスプローラ」→右クリック→「NuGetパッケージの管理」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405484/9c141229-7aa2-8bfd-a026-481b0df19ed2.png)

「System.Management」で検索→インストール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405484/314f3900-b3fb-67b6-4137-719570c249ba.png)

「SerialPort」で検索→インストール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405484/a7b48d53-3e10-32f0-5e0c-8de4881e4db6.png)


### Invokeについて

https://cammy.co.jp/technical/c_invoke/

http://okwakatta.net/code/trd02.html

サブスレッドからコントロールにアクセスするための方法。


```C#
void main_thread()
{
    ~process~
    
    invoke_func();   //invokeプロセスを呼び出す

    ~process~
}

private strint text;
void sub_thread()
{
    ~process~

    text = "changed";
    update();   //invokeプロセスを呼び出す。delegateを使う関係で引数はつけられない。
    
    ~process~
}

public delegate void DelegateUpdateText();

private void update()
{
    if(this.InvokeRequired)
    {
        //メインスレッドの場合は実行されない
        //サブスレッドの場合は実行される
        this.Invoke(new DelegateUpdateText(this.invoke_func));   //メインスレッドに処理を委譲する
        return;     //サブスレッドはreturnして以下は処理しない
    }

    //処理したい内容を以下に記述する。メインスレッドのみがたどり着く
}

private void invoke_func(){
    //コントロール内の変更等の処理
    this.label1.Text = text;
}

```

### delegate:代行者
なんかかっこいい英単語だなぁ。異世界転生しそうだ

https://ufcpp.net/study/csharp/sp_delegate.html

```C
public delegate void DelegateUpdateText();

var func_point = new DelegateUpdateText(this.invoke_func));

this.Invoke(func_point);
```

`delegate`型は関数ポインタみたいもの。

`this.Invoke(delegate)`で、メインスレッドでdelegateを処理する。


# 通信処理

## type1:英字＋数値（例"a001"）

### PC受信処理側

```C#:namespace
using System.Text.RegularExpressions;
```

```C#:受信処理内
int serial_int;
string serial_str = Regex.Replace(this.serial_text, @"[0-9]", "");
serial_str = serial_str.TrimEnd('\n','\r');
string serial_num = Regex.Replace(this.serial_text, @"[^0-9]", "");
bool boo = int.TryParse(serial_num, out serial_int);
if (boo)
{
    if (serial_str.Contains("a"))
    {
        this.label1.Text = serial_num;      
    }
    if (serial_str.Contains("b"))
    {
        this.label2.Text = serial_num;      
    }
    if (serial_str.Contains("c"))
    {
        this.label3.Text = serial_num;      
    }

}
```
処理内容

* 受信データ`serial_text`に対して0~9を消したもの→`serial_str`
* 受信データ`serial_text`に対して0~9以外を消したもの→`serial_txt`

### Arduino:送信

```C:送信処理
Serial.print("a");
Serial.println(count);
```

これだけでいい

```C:送信処理
Serial.print("a");
Serial.println(123);
```

```C:送信処理
Serial.print("a");
Serial.println("123");
```

同じ結果になる。

(備考)
`001`を`TryParse`すると`1`になる



### C#:PC側送信処理

```C#
SerialPort1.Write("a" + num.ToString() + "\n");
```
これだけ・・・
簡単

### Arduino側：受信処理

```C
if (Serial.available() > 0) {
    String str = Serial.readString();
    
    if (str.startsWith("a")) {
      int num = str.substring(1, str.length()).toInt();
    }
    if (str.startsWith("b")) {
      int num = str.substring(1, str.length()).toInt();
    }
}
```
先頭文字で場合分け、1文字目を取り除いたstringに対してint変換する。

## type2:英字＋数値+カンマ+数値+・・・
カンマで数値を大量に送信する手法。

### Arduino側：受信処理
ArduinoのStringに対する処理が貧弱なため大変・・・

https://algorithm.joho.info/arduino/string-split-delimiter/

こちら方の作ってくださった関数を使うのが一番でした（atoi,strtokなどうまくできなくて、無駄に苦労してしまった）。

```C:loop内
  if (Serial.available() > 0) {
    String str = Serial.readString();
    if (str.startsWith("a")) {
      String hoge[10] = {};
      int index = split(str.substring(1, str.length()), ',', hoge);
      for (int i = 0; i < index; i++) {
        Serial.println(hoge[i].toInt());
      }
      Serial.println("---------");
    }
  }
```

```C:変換関数
int split(String data, char delimiter, String *dst) {
  int index = 0;
  int datalength = data.length();

  for (int i = 0; i < datalength; i++) {
    char tmp = data.charAt(i);
    if ( tmp == delimiter ) {
      index++;
    }
    else dst[index] += tmp;
  }

  return (index + 1);
}
```
（コードの著作権的に良くない気がしてきた・・・）

`a1,2,3,4,5,6`を送ると`hoge[10]={"1","2","3","4","5","6"}`となる。
`a,2,3,4,5,6`を送ると`hoge[10]={"","2","3","4","5","6"}`となる。
`a1`を送ると`hoge[10]={"1"}`となる。

めっちゃいい動きをする。

---

`"001".toInt()`に対しては`1`が帰ってくる
`"".toInt()`に対しては`0`が帰ってくる
`"a".toInt()`に対しては`0`が帰ってくる
エラーすると0が返ってくるのかな・・・

# timed out対処
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405484/5f61e7db-e39b-6f05-36c3-fab7b4269787.png)

```C#
try
{
    serial_text = SerialPort1.ReadLine();
    invoke_func();

}
catch (TimeoutException)
{
    serial_text = "timeout";
    invoke_func();
}
catch (Exception ex)
{
    MessageBox.Show(ex.Message);
}
```
こうすると、timeoutエラーの対処になる。

# 終了時について
サブスレッドを消さないとプログラムが終了しない

# 完成形
Form1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405484/ea116cbe-497b-9720-f522-53bfe3a22c40.png)


Form2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405484/6acc7c7e-3bb5-5509-dc77-d6757f3dff6a.png)


* ”USB接続開始"を押すことでForm2を起動
* Form2内でCOMを選択
* Form1に帰ってきて、Serialデータを読む
* InvokeでTextボックスに読んだデータを入れる

```C#:Form1.cs
using System;
using System.Windows;
using System.IO.Ports;
using System.Text.RegularExpressions;


namespace pachi_conter
{
    public partial class Main : Form
    {
        static private SerialPort SerialPort1 = null;
        private String SerialState = "CLOSE";
        Thread readThread;
        static private bool _continue = false;

        public Main()
        {
            InitializeComponent();
            this.Text = "Arduino_COM";
            //SerialPort1.DataReceived += SerialPort1_DataReceived;
        }

        private void Main_Load(object sender, EventArgs e)
        {
            readThread = new Thread(Read);
            

        }

        private void button1_Click(object sender, EventArgs e)
        {
            COM_CONNECT f2 = new COM_CONNECT();
            //f2.Show();
            f2.ShowDialog();
            string selected_com_name = f2.selected_com_name;
            COM_NAME.Text = selected_com_name;

            if (selected_com_name != "")
            {
                //選択されたCOMと接続を試みる::接続できないときはエラーを出す
                //シリアルポートの初期化(接続設定)
                SerialPort1 = new SerialPort()
                {
                    BaudRate = 9600, //変調回数(1秒あたり)
                    DataBits = 8, // 1変調あたりのbit数
                    StopBits = StopBits.One,
                    Parity = Parity.None,
                    Handshake = Handshake.None,
                    DtrEnable = true,                //Arduinoの場合必要
                    RtsEnable = false,
                    PortName = selected_com_name,
                    ReadTimeout = 10000,
                    WriteTimeout = 10000,
                };

                try
                {
                    SerialPort1.Open();
                    SerialState = "OPEN";
                    //初期通信
                    System.Threading.Thread.Sleep(500);
                    SerialPort1.Write("a198\n");
                    COM_STATE.Text = "OPENED";

                    //SerialPort1.DataReceived += SerialPort1_DataReceived;
                    _continue = true;
                    readThread.Start();

                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }

                

            }

        }
        string serial_text="";
        public void Read()
        {
            while (_continue)
            {
                try
                {
                    serial_text = SerialPort1.ReadLine();
                    invoke_func();

                }
                catch (TimeoutException)
                {
                    serial_text = "timeout";
                    invoke_func();
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                }

            }
        }
        public delegate void DelegateUpdateText();
        public void invoke_func()
        {
            if (this.InvokeRequired)
            {
                //メインスレッドの場合は実行されない
                //サブスレッドの場合は実行される
                this.Invoke(new DelegateUpdateText(update));   //メインスレッドに処理を委譲する
                return;
            }

            //処理したい内容を以下に記述する
        }

        private void update()
        {
            this.SerialData.Text = this.serial_text;

            int serial_int;
            string serial_str = Regex.Replace(this.serial_text, @"[0-9]", "");
            serial_str = serial_str.TrimEnd('\n', '\r');
            string serial_num = Regex.Replace(this.serial_text, @"[^0-9]", "");
            bool boo = int.TryParse(serial_num, out serial_int);
            if (boo)
            {
                if (serial_str.Contains("a"))
                {
                    this.recived_type.Text = serial_str;
                    this.recived_word.Text = serial_num;
                }
                if (serial_str.Contains("b"))
                {
                    //this.label_serial_b.Text = serial_int.ToString();
                }
                if (serial_str.Contains("c"))
                {
                    
                }
            }
        }
        private void Main_FormClosing(object sender, FormClosingEventArgs e)
        {
            _continue = false;
            //readThread.Join();
            if (!(SerialPort1 == null))
            {
                SerialPort1.Close();
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            SerialPort1.Write(send_word.Text);
        }
    }
    
}
```

```C#:Form2.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO.Ports;
using System.Threading;
using System.Management;

namespace pachi_conter
{
    public partial class COM_CONNECT : Form
    {
        string[] com_num_text = new string[100];
        public string selected_com_name = "";
        public COM_CONNECT()
        {
            InitializeComponent();
            this.Text = "USB接続先選択";
        }

        private void Form2_Load(object sender, EventArgs e)
        {
            update();
        }
        public void update()
        {

            string[] ports = GetDeviceNames(ref com_num_text);

            listBox1.Items.Clear();
            listBox2.Items.Clear();
            int index = 0;
            foreach (string port in ports)
            {
                listBox1.Items.Add(port);                 //USB-SERIALなど名前
                listBox2.Items.Add(com_num_text[index]);  //COM3など
                index++;
            }
            if (listBox1.Items.Count > 0) listBox1.SelectedIndex = 0;

        }
        public static string[] GetDeviceNames(ref string[] com_num_text_array)
        {
            var deviceNameList = new System.Collections.ArrayList();
            var check = new Regex("(COM[1-9][0-9]?[0-9]?)");

            ManagementClass mcPnPEntity = new ManagementClass("Win32_PnPEntity");
            ManagementObjectCollection manageObjCol = mcPnPEntity.GetInstances();

            //全てのPnPデバイスを探索しシリアル通信が行われるデバイスを随時追加する
            foreach (ManagementObject manageObj in manageObjCol)
            {
                //Nameプロパティを取得
                var namePropertyValue = manageObj.GetPropertyValue("Name");
                if (namePropertyValue == null)
                {
                    continue;
                }

                //Nameプロパティ文字列の一部が"(COM1)～(COM999)"と一致するときリストに追加"
                string name = namePropertyValue.ToString();
                if (check.IsMatch(name))
                {
                    deviceNameList.Add(name);
                }
            }

            //戻り値作成
            if (deviceNameList.Count > 0)
            {
                string[] deviceNames = new string[deviceNameList.Count];
                int index = 0;
                foreach (var name in deviceNameList)
                {
                    var com_num = Regex.Match(name.ToString(), @"(COM[1-9][0-9]?[0-9]?)");
                    com_num_text_array[index] = com_num.ToString();
                    deviceNames[index++] = name.ToString();
                }

                return deviceNames;
            }
            else
            {
                return null;
            }
        }

        

        private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            listBox2.SelectedIndex = listBox1.SelectedIndex;
        }
        private void listBox2_SelectedIndexChanged(object sender, EventArgs e)
        {
            listBox1.SelectedIndex = listBox2.SelectedIndex;
        }
        private void button1_Click(object sender, EventArgs e)
        {
            update();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            selected_com_name = (String)listBox2.SelectedItem;
            this.Close();

        }
    }
}

```


```C:Arduino


#define COUNT 7
#define ROUND 3
#define RIGHT 4

volatile boolean count_flag = false;
volatile int array_1[10] = {} ;

void setup() {
  Serial.begin(9600);
  pinMode(2, INPUT);
  pinMode(3, INPUT);
  pinMode(4, INPUT);
  pinMode(7, INPUT_PULLUP);
}
void count_func() {
  if (digitalRead(COUNT) == LOW) {
    Serial.print("a");
    Serial.println("001");
    delay(200);
  }
}
void loop() {
  //2pin 以外にCOUNTを紐付けるときはここで探る
  if (digitalRead(COUNT) == LOW) {
    if ( count_flag == false) {
      count_flag = true;  //長押し＝連打
      count_func();
    }
  } else {
    count_flag = false;
  }

  if (Serial.available() > 0) {
    String str = Serial.readString();
    if (str.startsWith("a")) {
      String hoge[10] = {};
      int index = split(str.substring(1, str.length()), ',', hoge);
      for (int i = 0; i < index; i++) {
        Serial.println(hoge[i].toInt());
      }
      Serial.println("---------");
    }
  }
}

int split(String data, char delimiter, String *dst) {
  int index = 0;
  int datalength = data.length();

  for (int i = 0; i < datalength; i++) {
    char tmp = data.charAt(i);
    if ( tmp == delimiter ) {
      index++;
    }
    else dst[index] += tmp;
  }

  return (index + 1);
}

```

こんな感じ・・・