#12 テキストを扱おう&ファイルを読み書きしよう【もっと!後輩たちのためのEV3rt講座】

# 目次

| 章 | タイトル | 内容 |
| :-: | :-: | :-: |
| 1 | [はじめに](#1-はじめに) | 今回やることの説明 |
| 2 | [ファイルを開く](#2-ファイルを開く) | プログラムからテキストファイルを開く方法 |
| 3 | [テキストを書き込む](#3-テキストを書き込む) | ファイルにテキストを書き込む方法 |
| 4 | [テキストを読み込む](#4-テキストを読み込む) | ファイルからテキストを読み込む方法 |
| 5 | [データロギング実践](#5-データロギング実践) | 実践的なコードを紹介 |
| 6 | [まとめ](#6-まとめ) | 今回のまとめ |

# 1. はじめに

今回はEV3rtで用いることが出来る「標準Cライブラリ」をフル活用していきます！
EV3rtはLinuxマシンのような要領で、プログラム中からテキストファイルを開くことが出来ます。
そしてその開いたテキストファイルに対して、データを読み書きすることが出来るのです。

これが使えるようになると何が良いかといえば、 **プログラム実行中のログ** (センサ値などの記録)を、microSDカード内に **ファイルとして残す** ことができるようになるのです。

EV3ソフトウェアにも「データロギング」という機能が搭載されていたはずです。

![スクリーンショット 2025-01-03 141901.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/c45b69bb-88d2-d4d9-92ed-c2101564c55b.png)

これを、EV3rtでも実現しようというお話です。

実は、ロボットは動いているときと止まってるときでは、対象物が同じでもセンサ値が変わったりするんです。
ということは、停止状態のセンサ値で測定出来たつもりになっていると、走った時に誤差が生まれる要因となってしまいます。
こういったことを避けるためにも、データロギングの仕方をマスターしていきましょう!

# 2. ファイルを開く

ではまず、プログラムからファイルを開く方法から解説していきます。
使うのは **「ファイルポインタ」** と **「ファイルオープン関数」** です。

まずは **「ファイルポインタ」** について。
C言語における「ポインタ」について詳しく話し出すと、それだけで記事が一本書けてしまうので省略しますが、大雑把に言えば **「その変数がどこにあるかを指し示すもの」** といったところです。
今回の「ファイルポインタ」で言えば、読み書きしようとしているファイルが「どこにあるのか」を指し示します。

そして **「ファイルオープン関数」** は、その名の通りファイルを開く関数です。
この関数の戻り値は 「ファイルポインタ」 となっていますので、関数を実行した結果を 「ファイルポインタ」 に代入してあげればOKです。

では、この2つを使ってファイルを開いてみましょう。

```c:app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    //プログラムをここから書く
    FILE *fp = fopen("test.txt", "w"); // ファイルを開く関数
    
    fclose(fp); // ファイルを閉じる関数
}
```

## ファイルポインタ

まず8行目の先頭の以下の部分について。

```c
FILE *fp
```

これがファイルポインタの定義です。
`FILE` は構造体の名前を意味しますが、変数の型である`int型`や`char型`の仲間と思っていただいて結構です。
後ろの `fp` が名前で、 `*` が **「ポインタ」** であることを示します。

## fopen() 関数

続いて、ファイルオープン関数について。

```c
fopen("test.txt", "w");
```

ファイルを開く `fopen()` 関数は、第一引数に「ファイルの場所」、第二引数に「モード」を指定します。
「ファイルの場所」はその名の通りですが、EV3rtの場合、SDカードの一番親フォルダ (`ev3rt`フォルダや`uimage`ファイルがあるフォルダ) が起点となります。
今、単純に `"test.txt"`と指定しているので、`ev3rt`フォルダや`uimage`ファイルと並んで `test.txt` ファイルが作成されることとなります。

そして「モード」についてですが、ファイルを開く際に「読み込み」をするのか、あるいは「書き込み」をするのか、またはその両方を行うのかを指定する必要があります。
指定方法は6種類あり、以下の通りとなっています。

| モード | 動作 | ファイル存在時の動作 | ファイルが存在<br>しない時の動作 |
|:-:|:-:|:-:|:-:|
| r | 読み込み専用 | 読み込む | エラー | 
| w | 書き込み専用 | 上書き | 新規作成 | 
| a	| 追記書き込み専用 | ファイルの末尾に追記 | 新規作成 | 
| r+ | 読み込みと書き込み | 通常の動作 | エラー | 
| w+ | 書き込みと読み込み | 上書き | 新規作成 | 
| a+ | 追記書き込みと読み込み | ファイルの末尾に追記 | 新規作成 | 

(引用: https://bituse.info/c_func/26)

今回の `w` であれば、ファイルを新規作成 (もしくは上書き) し、テキストを書き込んでいくということになります。
読み込みをしたい場合や、読み書き両方をしたい場合は適宜モードを切り替えてください。

`fopen()` 関数の詳しい説明はこちら👇を参照してください。

https://bituse.info/c_func/26

## fclose() 関数

最後に、プログラム末尾で `fclose()` 関数を使用していますが、名前から想像がつく通りファイルを閉じる関数です。引数には「ファイルポインタ」指定します。
このファイルを閉じる動作をしておかないと、データが正しく保存されない事があるので注意しましょう。

# 3. テキストを書き込む

では、実際にファイルにテキストを書き込んでいきましょう。
ファイルにテキストを書き込む関数はいくつか種類がありますが、最も汎用的な関数である `fprintf()` 関数を紹介したいと思います。

## fprintf() 関数

まずは全体のコードから。

```c:app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    //プログラムをここから書く
    int i;
    FILE *fp = fopen("test.txt", "w"); // SDカードのルートディレクトリにtest.txtを作成
    fprintf(fp, "Hello, World!\n"); // test.txtにHello, World!と書き込む
    for (i = 0; i < 10; i++)
    {
        fprintf(fp, "i = %d\n", i); // test.txtにi = 0, i = 1, ... , i = 9と書き込む
        tslp_tsk(1*1000*1000); // 1秒待つ
    }
    
    fclose(fp); // ファイルを閉じる
    ext_tsk();
}
```

上記コードの10行目、および13行目に使われているのが `fprintf()` 関数です。
ファイルにテキストを書き込む関数で、第一引数に「ファイルポインタ」、第二引数に「書式に基づいた文字列」、必要に応じて第三引数以降に「文字列に組み込む変数」を指定します。

ここで、第二引数の「書式に基づいた文字列」について説明しておくと、この `fprintf()` 関数は出力したい文字列の中に任意の変数の値を組み込むことが出来ます。
10行目の例では、単純に `"Hello World"` だけ書き込んでいますが、13行目では途中に `%d` が混ざっています。この `%d` の部分に、第三引数に指定した変数`i`の値を埋め込むということになります。

変数の埋め込み方はデータ型によって異なりますので、以下を参考に書式を設定してください。
(とりあえず、良く使いそうな指定子だけ抜き出しています。)

| 書式変換指定文字 | 意味 | 主な変数の型 |
|:-:|:-:|:-:|
| %d | 10進数で出力 | int型 |
| %f | 浮動小数点で出力 | float型 |
| %c | 文字を出力 | char型 |
| %s | 文字列を出力 | char型配列 |

このフォーマット指定子はかなり多くの種類があり、細かく設定すれば「〇桁に固定する」なども出来るので、以下のサイトを参考に自分が見やすいように指定してみてください。

https://www.k-cube.co.jp/wakaba/server/format.html

## プログラムを実行

では先ほどのプログラムをコンパイルし、実行してみましょう。
今回、LCD等に何も表示していないので、プログラム実行後、自分で10秒ほど数えて下さい。
(余裕があれば、過去回を参考にLCDに`i`の値を表示したり、終了したらボタンの色を変えてみるなど、分かりやすいように改造してみてください。)

10秒経過したらプログラムを停止し、EV3にUSBケーブルを接続してファイルが作成されているか確認してみましょう。

![スクリーンショット 2025-01-03 175005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/df20e31d-f7d9-23a6-c84e-ef8e8becc928.png)

SDカードの一番上のディレクトリに `test.txt` が作成されていることが確認できたでしょうか。
中身についても確認しておきましょう。

![スクリーンショット 2025-01-03 175130.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/f09b0fd1-3b94-b9dc-4de5-8011a5ded1d3.png)

しっかりプログラム通りに動いていますね！
以上で、ファイルへの書き込みは完了です！！

### 他の書き込みに使える関数一覧

- [`fputc()`](https://bituse.info/c_func/30) : 1文字の書き込み (変数の組み込みは不可)
- [`fputs()`](https://bituse.info/c_func/29) : 複数文字の書き込み (変数の組み込みは不可)
- [`fwrite()`](https://bituse.info/c_func/44) : 複数文字の書き込み (変数の組み込みは不可)

# 4. テキストを読み込む

今度は、今作成した `test.txt` ファイルからテキスト読み出してみましょう。
個人的な感覚ですが、書き込みは `fprintf()` 関数一つあれば事足りますが、読み出しに関してはいくつかの関数を使い分けた方が良いかな…と思うタイプですので、今回は用途に分けて3つ紹介したいと思います。

## fgetc() 関数

まずは「1文字読み出す」関数である `fgetc()` 関数からです。

```c:app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    //プログラムをここから書く
    int txt;
    char str[16];
    ev3_lcd_set_font(EV3_FONT_MEDIUM);

    FILE *fp = fopen("test.txt", "r"); // SDカードのルートディレクトリのtest.txtを読み込み
    txt = fgetc(fp);
    sprintf(str, "txt = %c", txt);
    ev3_lcd_draw_string(str, 0, 0);
    fclose(fp); // ファイルを閉じる
    ext_tsk();
}
```

まず、12行目の `fopen()` 関数のモードが読み込みモード `r` に変わっていることに注意してください。

13行目に使われているのが `fgetc()` 関数で、引数には「ファイルポインタ」を指定、そのファイルから1文字読み出して、`int型`で読み込んだ文字を返します。
(C言語では、単一文字をしばしば`int型`で表しますので注意しましょう)

正しく読み込めたかを確認したいので、`sprintf()` 関数を用いて、変数の値を`char型`配列 `str` に格納しています。

少し脱線しますが、この `sprintf()` は先ほどの `fprintf()` 関数の仲間で、書式つきの文字列を `char型` 配列に格納する関数となっています。第一引数に保存先の `char型` を指定します。書式の指定方法は `fprintf()` 関数と全く同じです。

この関数を使うと、任意の変数を `char型` 配列内に文字列として保存することが出来、`ev3_lcd_draw_string()` 関数によりEV3のLCD上に表示する事が出来るのです。
LCDを紹介した回であまり詳しく説明しなかったので補足しておきます。

話を戻しますと、このプログラムは `test.txt` から1文字読み出し、LCDに `txt = 〇` という形で出力するという流れになります。
ではプログラムをコンパイルし、実行してみましょう。

無事 `txt = H` と表示されたでしょうか？
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/9986d34c-6957-c731-6d21-f060ac53cd3a.jpeg" width = 60%>

ちなみに、 `fgetc()` 関数は常にファイルの先頭から1文字読み出すわけではなく、順に次の文字を読み出します。
そのような動作をしているかも、以下のプログラムにより確認してみましょう。

```c:app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    //プログラムをここから書く
    int i;
    int txt;
    char str[16];
    ev3_lcd_set_font(EV3_FONT_MEDIUM);

    FILE *fp = fopen("test.txt", "r"); // SDカードのルートディレクトリのtest.txtを読み込み

    for (i = 0; i < 13; i++)
    {
        txt = fgetc(fp);
        sprintf(str, "txt = %c", txt);
        ev3_lcd_draw_string(str, 0, 0);
        tslp_tsk(500*1000); // 0.5秒待つ
    }
    
    fclose(fp); // ファイルを閉じる
    ext_tsk();
}
```

先ほどテキストに書き込んだ `Hello, World!` の文字が一文字ずつ表示されるはずです！
`fgetc()` 関数についてもっと詳しく知りたい方は以下のサイトを参考にしてみてください。

https://bituse.info/c_func/31

## fgets() 関数

続いては、複数文字からなる文字列を一気に読み出す `fgets()` 関数です。
ここでも全体のコードを見ながら確認していきましょう。

```c:app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    //プログラムをここから書く
    int i;
    int txt;
    char str[16];
    ev3_lcd_set_font(EV3_FONT_MEDIUM);

    FILE *fp = fopen("test.txt", "r"); // SDカードのルートディレクトリのtest.txtを読み込み
    fgets(str, 16, fp);
    ev3_lcd_draw_string(str, 0, 0);
    fclose(fp); // ファイルを閉じる
    ext_tsk();
}
```

14行目に使われているのが `fgets()` 関数で、1行分のテキストを読み出して、`char型`配列に格納します。
第一引数には「格納先の`char型`配列」、第二引数には「読み込む最大文字数」、第三引数には「ファイルポインタ」を指定します。
第二引数の「読み込み最大文字数」については、読み出した文字列が `char型` 配列に格納し切れず、溢れてしまうことを防止するためのものなので、基本的には配列の長さを指定すれば良いと思います。
尚、この関数は1行分読み込んだらそこでストップするので、かならず最大文字数分読み込む訳では無いので注意しましょう。

ではこのプログラムをコンパイルし、実行してみましょう。
すると、以下のように表示されたのではないでしょうか。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/69630649-841b-27e9-758e-a85446cadd10.jpeg" width = 60%>

`Hello, World!` までは良いものの、末尾に `?` がついていますね。
これは「改行コード」が表示されている状態です。
先ほど `fprintf()` を使ったときに、以下のように書き込みを行いましたね？

```c
fprintf(fp, "Hello, World!\n");
```

ここでの `\n` が改行コードであり、ファイル中で改行をする印となっています。
本来 `\n` は見えない文字のため、PCなどでは表示されませんが、EV3上では `?` と表示される仕様となっています。
改行コード以外にも、「見えない文字」はいくつか種類があり、それらもEV3上で `?` と表示されるので覚えておきましょう。

以上で、複数文字(1行分)の読み出しは完了です！
`fgets()` 関数についてもっと詳しく知りたい方は以下のサイトを参考にしてみてください。

https://bituse.info/c_func/28

## fscanf() 関数

最後に紹介するのは、書き込みに使った `fprintf()` 関数の対となるような関数、`fscanf()` 関数です。
この関数を使うと、「指定した書式に基づいて文字列を読み出し、変数に格納する」といった事が出来ます。

```c:app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    //プログラムをここから書く
    int i, num;
    char str[16];
    char str2[16];
    ev3_lcd_set_font(EV3_FONT_MEDIUM);

    FILE *fp = fopen("test.txt", "r"); // SDカードのルートディレクトリのtest.txtを読み込み

    fgets(str, 16, fp);
    ev3_lcd_draw_string(str, 0, 0);
    tslp_tsk(1*1000*1000); // 1秒待つ

    ev3_lcd_fill_rect(0, 0, EV3_LCD_WIDTH, EV3_LCD_HEIGHT, EV3_LCD_WHITE);

    for (i = 0; i < 10; i++)
    {
        fscanf(fp, "i = %d\n", &num);
        sprintf(str2, "num = %d", num);

        ev3_lcd_draw_string(str2, 0, 0);
        tslp_tsk(1*1000*1000); // 1秒待つ
    }

    fclose(fp); // ファイルを閉じる
    ext_tsk();
}
```

まず `test.txt` ですが、1行目に `Hello, World!` 、2行目以降に `i = 〇` が書かれているので、とりあえず`fgets()` 関数で1行目を読み出しておきましょう。( `app.c` の15行目)

そして、その後の `for文` で `i = 〇` の部分を読み出していくのですが、そこで使うのが `fscanf()` 関数です。

```c
fscanf(fp, "i = %d\n", &num);
```

第一引数に「ファイルポインタ」、第二引数に「書式に基づいた文字列」、必要に応じて第三引数以降に「代入先の変数 (のアドレス) 」を指定します。
書式の指定方法は `fprintf()` 関数と同じです。
今回の例で言えば、`%d` の部分にあるであろう整数が、 `int型` 変数 `num` に代入されます。
ここで、第三引数には変数の **アドレス(変数がある場所)** を指定するので、`&`を付けるのを忘れないようにしてください。

ではこのプログラムをコンパイルし、実行してみましょう。
始めは先ほど同様 `Hello, World!?` と表示され、その後に `num = 0` 、`num = １` 、… `num = 9` と表示されれば成功です！

さてこの `fscanf()` 関数をはじめとする `scanf()` 系の関数ですが、文字列を読み込んで変数に格納してくれるので便利な反面、実は思ったよりも扱いが難しいです…
かなりバグが発生しやすい関数ですので、乱発は避けて、注意して使っていきましょう。

詳しく知りたい方は以下のサイトを参考にしてみてください。

https://bituse.info/c_func/42

### 他の書き込みに使える関数一覧

- [`fread()`](https://bituse.info/c_func/43) : 複数文字の書き込み

# 5. データロギング実践

最後に、より実践的なコードでデータロギングを行ってみましょう。
今回はロボットが直進している間のモーターの回転数と、カラーセンサーの値をファイルに書きこんでいきたいと思います。

実験フィールドとして、以下のようなコースを用意してみました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/27c40257-7a54-aaf6-940c-0378cebf9d1b.jpeg" width=65%>
間隔を空けてブロックが置かれており、このブロックの色を順に読み取っていきます。

そして、単純なテキストファイルではなく **CSVファイル** として保存し、実行後の解析がしやすいようにしておきたいと思います。
CSVファイルとは、各項目がカンマ`,`で区切られているファイルであり、Excelなどの表計算ソフトに簡単に取り込むことが出来ます。

では、コードの紹介です。

```c:app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    FILE *fp;
    int i = 0;

    ev3_motor_config(EV3_PORT_B, LARGE_MOTOR); // モーターBをLモーターに設定
    ev3_motor_config(EV3_PORT_C, LARGE_MOTOR); // モーターCをLモーターに設定

    ev3_sensor_config(EV3_PORT_3, COLOR_SENSOR); // カラーセンサーをポート3に設定

    ev3_motor_reset_counts(EV3_PORT_B); // モーターBの角位置をリセット
    ev3_motor_reset_counts(EV3_PORT_C); // モーターCの角位置をリセット

    // ファイルを開く
    fp = fopen("log.csv", "w"); // SDカードのev3rt/logディレクトリにlog.csvを作成
    if (fp != NULL)
    {
        fprintf(fp, "count,motorB,motorC,Color\n"); // ヘッダーを書き込む

        ev3_motor_set_power(EV3_PORT_B, 30); // モーターBのパワーを30に設定
        ev3_motor_set_power(EV3_PORT_C, 30); // モーターCのパワーを30に設定

        while(ev3_motor_get_counts(EV3_PORT_B) < 360 * 5)
        {
            int motorB = ev3_motor_get_counts(EV3_PORT_B); // モーターBの角位置を取得
            int motorC = ev3_motor_get_counts(EV3_PORT_C); // モーターCの角位置を取得
            int color = ev3_color_sensor_get_color(EV3_PORT_3); // カラーセンサーの値を取得
            fprintf(fp, "%d,%d,%d,%d\n", i, motorB, motorC, color); // ログを書き込む
            tslp_tsk(10*1000); // 10ms待つ
            i++;
        }

        ev3_motor_stop(EV3_PORT_B, true); // モーターBを停止
        ev3_motor_stop(EV3_PORT_C, true); // モーターCを停止

        fclose(fp); // ファイルを閉じる
    }
}
```

コードの細かい説明は省略しますが、要約すればモーターが5回転する間の角度、カラーセンサの値を、0.01秒ごとにファイルに書き込む、という内容になっています。

ではこのコードを実行し、ログを取ってみましょう。

実行したらUSBケーブルを挿し、中身を確認してください。
![スクリーンショット 2025-01-04 153830.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/2ded49e4-37e5-2704-b623-2aef7e2ea3cd.png)

`log.csv` ファイルが作成されているのが確認出来るでしょうか。
では、ファイルを開いて内容を確認していきます。もしPCにExcel等が入っているようでしたら、そちらで開いてみましょう。

![スクリーンショット 2025-01-04 155744.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/2c3e39f7-d7da-7257-e78b-ce3851ccf912.png)

CSVで保存したので、カンマで区切ったところで列が分かれているかと思います。
そして、せっかくExcelを使っているのでグラフ化もしてみましょう。

![スクリーンショット 2025-01-04 160936.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183170/e1418774-8b60-a6ef-47e6-94c08b81d4e5.png)

カラーセンサーの値と色の対応、さらに置かれていたブロックの色を挿入しておきました。
こう見てみると、ブロックの端を読んでいる時は `1` で黒色判定になっており、しっかり真ん中を捉えていると正しい色を判定していることが分かります。
黄色ブロックに関しては `黒→白→黄→白→黒` となっていますね。

このように、データロギングをしてみると新たに分かることもあります。
是非データロギングを活用して、センサーの動作やプログラムの改善点を見つけてみましょう！

# 6. まとめ

今回は、テキストデータの扱い方とファイルへの読み書きについて解説していきました。
EV3rtでのプログラムの幅がまた大きく広がったかと思います！

さて次回は、Bluetoothの使い方について2回に渡り紹介していきます！
今までコンパイルした実行ファイル `app` を有線でEV3に転送していましたが、ついに無線で転送出来るようになります！！

前回: [#11 タスクと周期ハンドラを使おう](https://qiita.com/koushiro/items/3b2785fbc94c308452a8)
次回: [#13 Bluetooth接続をしよう](https://qiita.com/koushiro/items/6e90accc9bc1ce02b1c5)