Help us understand the problem. What is going on with this article?

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

はじめに

 前回こちらの記事でFlutterのnfc_managerを使用して学生証のプリペイド残高を読み込んでみました。
 今回は、残高だけでなく、使用履歴も表示させていこうかと思います。ちなみに、デザインは全く意識せず、必要なデータだけ表示させてるので見た目に関してはシーッ!!!

完成形

gakuseisho.gif

やってみる

使用ライブラリ

 NFC通信を行うために使用するライブラリは「nfc_manager」というものを使っていきます。NFC通信をするためのライブラリは他にもいくつかありましたが、個人的にこちらのライブラリが一番使いやすかったのでこれにしています。

説明

 NFCのコマンドについてはFelicaカード ユーザーズマニュアルを参考にしていただければOK
 また、今回のNFC通信先である大学生協のデータ構造については、大学生協Felicaの仕様にまとめられていますので、他の項目を取得する際には参考にしてみてください。

コード

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作業など基本的なことはこちらの記事で説明しています。(といっても軽く雑に)詳細まで知りたい方はマニュアルを読んでください。

サービスコードの変更

 前回の記事では、サービスコードに[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バイトに拡張して取得とかいろいろ試してみる必要がある(変換方法がサービスコードごとに取得できるかもしれないけど)
 まあ、自力で解読するのはまじで死ぬので(経験者は語る)、公開されている情報だけで我慢するのが妥当かも。白髪が増えるだけだぜ!
 それでは、またな!!

f-nakahara
はじめまして。鹿児島大学で学生やってます! プログラミングを2019年4月頃からです。まだまだ初心者ですが、参考にしていただければ嬉しいです! Webアプリ、ネイティブアプリ開発に関する情報を載せていく予定です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした