カンマ区切りのデータをarduinoのシリアルで受け取り配列に入れたい



![SS 192.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565043/a41cd967-f549-8677-1efa-2e99bf69518d.png) 長いコードになっちゃうけど万能な方法かも

##やること
Processingなど外部から送られたカンマ区切りのシリアルデータを、Arduinoで受信して配列に格納します。
今回は数値だけでなく大きな数字や長い文字列も格納できるような万能タイプの受信方法にしてみます。


##ポイント
カンマ区切りを処理できるstrtokを使いたいのですが、それには一旦charにする必要があり、またポインタも使う必要があります。やりたいことはごくシンプルですが、Arduinoではいろいろと厄介な部分があります。
（2021.02.14 補足２にて、strtokを使わないよりスマートな方法の追記がございます。）


##スケッチ

```cpp:Arduino系

#define elements 10 //カンマで区切るデータの最大項目数
String data_string; //シリアルで受け取る全文字列
char *p; //文字列をカンマで分割するstrtok処理で使うポインタ
String p_string; //上記ポインタで区切った文字列の仮格納用
String data_array[elements]; //カンマ分割されたstrデータを格納する

void setup() {
  Serial.begin(9600); //シリアルを開く
}

void loop() {
  if (Serial.available() > 0 ) {
    data_string = Serial.readStringUntil(0x0a); //シリアルデータを改行記号が現れるまで読み込む
    data_string.trim(); //文字列を念のためトリミングする
    int data_len = data_string.length() + 1; //str→char変換用にデータの長さを調べる
    char data_char[data_len]; //str→char変換用のchar配列を準備
    data_string.toCharArray(data_char, data_len); //ようやくstr→charに変換
    p = strtok(data_char, ","); //カンマ分割の1要素目を行う
    p_string = p; //一旦strにいれる
    data_array[0] = p_string; //最終目的の配列に1要素目を格納

    for (int i = 1; i < elements; i++) { //2要素名以降について、要素数分だけデータ配列に格納
      p = strtok(NULL, ","); //カンマ分割の2要素目以降のstrtokはこの書式になる
      if (p != NULL) { //要素が空でない場合はその要素をデータ配列に格納
        p_string = p;
        data_array[i] = p_string;
      } else {
        data_array[i] = "0"; //要素が空の場合はデータ配列に0を格納
      }
    }
    //データを表示する
    for (int i = 0; i < elements ; i++) {
      Serial.print(data_array[i]); //データ配列の中身を0項から最終項まで表示する
      Serial.print("/"); //カンマ区切りではなくスラッシュ区切りで表示
    }
    Serial.println();
  }
}
```

### 使い方
Arduinoで実行後、シリアルモニタからカンマ区切りの10個以内の文字列を入力し送信します。
Arduinoは受け取ったデータを配列data_array[]に格納し、その中身をスラッシュ区切りでシリアルに出力します。
データを受け取らなかった分については配列に0を格納します。
処理したいデータの要素数は、冒頭の#define elements の数字で変更できます。
設定した要素数を超えた分のデータ入力があった場合は、超過分を無視します。
シリアルモニタ以外にもprocessingから受け取ったカンマ区切りデータなども処理できます。

### 補足1
実際の利用シーンでは文字列データを数値に変換するなど用途に応じて配列の中身を取り出して加工してください。
数値しか扱わない場合はBYTEサイズにデータを圧縮してからwriteしたり、INTサイズの場合でもBYTEで送ってエンディアンをひっくり返すやり方の方が低コストにできます。
今回のスケッチはとりあえずなんでもかんでもカンマ区切りで受け付けちゃおうという方法になります。（コードはもう一声シンプルにできるかも。）

### 補足2

@KeitetsuWorks さんより、strtok()を使わないスマートな方法をご指摘いただきましたのでコメント欄より転載させていただきます。（@KeitetsuWorksさん、ありがとうございます。）

```cpp:Arduino系
/**
 * @file        qiita_6a18249a31a87dad84e2.ino
 * @brief       Arduino Sample Code
 * @author      Keitetsu
 * @date        2021/02/14
 * @copyright   Copyright (c) 2021 Keitetsu
 * @par         License
 *              This software is released under the MIT License.
 */

#define ELEMENTS_NUM 10 /**< カンマ区切りデータの項目数 */

/**
 * @brief       受信済み文字列を格納する配列
 */
static String elements[ELEMENTS_NUM];
static int received_elements_num = 0; /**< 受信済み文字列の数 */

/**
 * @brief       セットアップ関数
 */
void setup()
{
    Serial.begin(9600);
}

/**
 * @brief       ループ関数
 */
void loop()
{
    String line;              // 受信文字列
    unsigned int beginIndex;  // 要素の開始位置

    // シリアルモニタやProcessingから"AB,C,DEF,12,3,45,A1,2B,-1,+127"のように
    // ELEMENTS_NUM個の文字列の間にカンマを付けて送る
    // 送信側の改行設定は「LFのみ」にすること
    // シリアル通信で1行（改行コードまで）読み込む
    line = Serial.readStringUntil('\n');

    beginIndex = 0;
    for (received_elements_num = 0; received_elements_num < ELEMENTS_NUM; received_elements_num++) {
        // 最後の要素ではない場合
        if (received_elements_num != (ELEMENTS_NUM - 1)) {
            // 要素の開始位置から，カンマの位置を検索する
            unsigned int endIndex;
            endIndex = line.indexOf(',', beginIndex);
            // カンマが見つかった場合
            if (endIndex != -1) {
                // 文字列を切り出して配列に格納する
                elements[received_elements_num] = line.substring(beginIndex, endIndex);
                // 要素の開始位置を更新する
                beginIndex = endIndex + 1;
            }
            // カンマが見つからなかった場合はfor文を中断する
            else {
                break;
            }
        }
        // 最後の要素の場合
        else {
            elements[received_elements_num] = line.substring(beginIndex);
        }
    }

    // 受信済み文字列の数がELEMENTS_NUM以上の場合
    if (received_elements_num >= ELEMENTS_NUM) {
        // 受信済み文字列をスラッシュ区切りでシリアルモニタに送信する
        for (int i = 0; i < ELEMENTS_NUM; i++) {
            Serial.print(elements[i]);
            Serial.print("/");
        }
        Serial.println();
    }
}
```
参考情報
[String.indexOf()](https://www.arduino.cc/reference/en/language/variables/data-types/string/functions/indexof/)
[String.substring()](https://www.arduino.cc/reference/en/language/variables/data-types/string/functions/substring/)
[サンプルコードのGist](https://gist.github.com/KeitetsuWorks/86de00fbad889d649937b94235ef4d0d)