FlutterでNFC通信を利用して学生証のプリペイド使用履歴を表示させてみた

# はじめに
　前回[こちら](https://qiita.com/f-nakahara/items/82fefe574a2e5a25182b)の記事でFlutterのnfc_managerを使用して学生証のプリペイド残高を読み込んでみました。
　今回は、残高だけでなく、使用履歴も表示させていこうかと思います。ちなみに、デザインは全く意識せず、必要なデータだけ表示させてるので見た目に関してはｼｰｯ！！！

# 完成形
![gakuseisho.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600842/edb83bbe-f093-e844-6671-46dadbc2578f.gif)


# やってみる
## 使用ライブラリ
　NFC通信を行うために使用するライブラリは「[nfc_manager](https://pub.dev/packages/nfc_manager)」というものを使っていきます。NFC通信をするためのライブラリは他にもいくつかありましたが、個人的にこちらのライブラリが一番使いやすかったのでこれにしています。

## 説明
　NFCのコマンドについては[Felicaカード ユーザーズマニュアル](https://www.sony.co.jp/Products/felica/business/tech-support/st_usmnl.html)を参考にしていただければOK
　また、今回のNFC通信先である大学生協のデータ構造については、[大学生協Felicaの仕様](https://gist.github.com/oboenikui/ee9fb0cb07a6690c410b872f64345120)にまとめられていますので、他の項目を取得する際には参考にしてみてください。

## コード
```main.dart
import 'package:flutter/material.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:nfc_manager/platform_tags.dart';

import 'gakuseiNFC.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver{ // アプリの離脱・復帰の検知用
  int balance = 0; // 残高
  var history = []; // 使用履歴
  NfcF nfcf;
  FeliCa felica;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    _readTag();
  }

  @override
  Widget build(BuildContext context) {
    _readTag();

    return Scaffold(
      appBar: AppBar(
        title: Text("学生証のプリペイド残高を読み込むぜ！"),  // アクションバーのタイトル
      ),

//      NFCに対応しているかを確認
      body: Column(
        children: <Widget>[
          Flexible(
            flex: 1,
            child: FutureBuilder(
              future: NfcManager.instance.isAvailable(),  // これでNFCに対応しているか確認する
              builder: (context,ss){  // 値が返ってきたら次の画面を描画する

                // 対応していない時（ss.data == false）
                if(!ss.data){
                  return Center(
                    child: Text("対応していません"),
                  );
                }

                // 対応しているとき（ss.data == true ）
                else{
                  return Center(
                    child: Text(
                      "$balance円",
                      style: TextStyle(
                        fontSize: 60,
                      ),
                    ),
                  );
                }
              },
            ),
          ),
          Flexible(
            flex: 2,
            child: Container(
              child: ListView.builder(
                itemCount: history.length,
                itemBuilder: (context,index){
                  return Container(
                    decoration: BoxDecoration(
                      border: Border.all(width: 0.2),
                    ),
                    padding: EdgeInsets.all(10),
                    child: Text(
                        "${(history[index][7]=="05")?"-":"+"}¥${int.parse(history[index][8]+history[index][9]+history[index][10])}円",
                      style: TextStyle(
                        fontSize: 40,
                      ),
                      textAlign: TextAlign.end,
                    ),
                  );
                }
              ),
            ),
          ),
        ],
      ),
    );
  }

  // 学生証読み込み
  _readTag(){
    NfcManager.instance.startTagSession(onDiscovered: (NfcTag tag) async {
      nfcf = NfcF.fromTag(tag);

      // NfcFに対応してなかったら
      if(nfcf == null){
        print("効果は無いみたいだ・・・");
      }

      // NfcFに対応していたら
      else{
        print("効果は抜群だ！！");
        var balance = await getBalanceData(nfcf); // プリペイド残高の取得
        var history = await getUsingHistory(nfcf);
        setState(() {
          this.balance = balance; // 残高データの更新
          this.history = history; // 使用履歴の更新
        });
      }
      NfcManager.instance.stopSession();  // 読み込み終了
    });
  }
}

```
　特に大したことはしていません。学生証を読み込んで、通信が完了次第残高や使用履歴を表示させます。
　データは配列で返ってきてるので、その中から必要な部分だけを表示させてます。今回使用したのは支払い・チャージか、使用金額の2つのデータだけ使用しました。支払いだったら「-」チャージだったら「+」を表示させ、金額部分は文字列からint型に変換して表示しています。（001000円みたいな表記になると個人的に気持ち悪いから）

```gakuseisho.dart

...略

// 使用履歴の取得
getUsingHistory(NfcF nfcf) async {
  List<int> SYSTEM_CODE = [0xFE,0x00];  // システムコード
  List<int> SERVICE_CODE = [0x50,0xCF]; // サービスコード
  int SIZE = 10;  // 取得数（最大10）

  var poll = await _polling(SYSTEM_CODE);
  var pollingRes = await nfcf.transceive(poll);
  var targetIDm = await pollingRes.sublist(2,10);
  var req = await _readWithoutEncryption(targetIDm, SIZE, SERVICE_CODE);
  var res = await nfcf.transceive(req);
  var data = _parse(res);

  var history = []; // 使用履歴用の配列
  // 取得してきたデータ数だけhistoryに追加する
  for(var i=0;i<data.length;i++){
    history.add([]);  // 空の配列を追加
    for(var j=0;j<data[i].length;j++){
      int val = data[i][j]; // 数値をとりあえず変数に格納
      history[i].add((val.toRadixString(16)).padLeft(2,"0")); // 16進数表記に変換してhistoryに追加
    }
  }
  return history;
}

... 略
```
　polling作業や、parse作業など基本的なことは[こちらの記事](https://qiita.com/f-nakahara/items/82fefe574a2e5a25182b)で説明しています。（といっても軽く雑に）詳細まで知りたい方はマニュアルを読んでください。
### サービスコードの変更
　前回の記事では、サービスコードに[0x50,0xD7]を指定しました。このコードは現在の残高や使用回数が入っています。
　今回取得したいのは使用履歴なので、[0x50,0xCF]を指定します。

# 16進数に変換
```gakuseisho.dart
history[i].add((val.toRadixString(16)).padLeft(2,"0"));
```
　この部分では数値を16進数に変換してます。前回残高を取得する際は、データがリトルエンディアン形式で保管されていたため、取得してきた数値をひっくり返して8バイトから32バイトに拡張して変換みたいな面倒くさいことをしていました。
　今回扱うデータは16進数に変換した数値を10進数とみなして読み取ればいいため、16進数に変換しています。例えば、16進数表記で[20,20,3,12]とあった場合、わざわざ10進数に直したりせず、このまま読み取ればいいです。（この場合だと「2020/3/12」という日付を表している）
　また、変換後は1桁になった場合に0が消えないように、0で桁を埋めます。（1→01）こうしないと、1000円をあらわすときに[10,00]となるべきところで[10,0]と0が1桁になってしまって100円になってしまいます。これが給料貰うときに発生したらたまらんな！（なんの話や）

# 最後に
　とまあ、今回は簡単に使用履歴を取得してみました。NFC通信は理解するまでにめちゃめちゃ苦しみますが、理解した後は欲しい情報は割とすぐに取得できるようになると思います。
　また、データ構造は基本的に公開されていないため、解読できてない数値は自分でUTF-8変換やビックエンディアン形式で32,64バイトに拡張して取得とかいろいろ試してみる必要がある（変換方法がサービスコードごとに取得できるかもしれないけど）
　まあ、自力で解読するのはまじで死ぬので（経験者は語る）、公開されている情報だけで我慢するのが妥当かも。白髪が増えるだけだぜ！
　それでは、またな！！