【Java】Android Studio でバーコードリーダーを実装したい

この記事は、群馬高専AdventCalendar14日目の記事です。

Android Studioを使って、バーコードリーダを作成していきたいと思います。
また、作成するにあたって押さえておきたい点も以下に列挙します。

  1. カメラ画面のカスタマイズ
  2. アクセス許可を尋ねるダイアログの表示
  3. JANコードのみを認識するようにする
  4. 連続的な読み取りを可能にする

この三つです。それぞれ、順番に説明いたします。

アクセス許可のダイアログ表示

よくあるのは、「カメラのアクセスを許可しますか」とか、「マイクのアクセスを許可しますか」とか、「位置情報の利用を許可しますか」といったやつです。アプリが、スマホに実装されているUIを利用することを許可するか否かを聞いてくるダイアログはよく見かけると思います。今回はせっかくなので「カメラのアクセスを許可しますか」的なダイアログを表示させて、許可をタップしたらアプリの開始、許可をしなかったらアプリが終了する、という簡単なプログラムを組んでみたいと思います。

JANコードのみを認識させる

バーコードと一言にいっても、QRコードなどの二次元コードから、JANコードITFコードなどの様々なバーコードがありますが、これらを自動的に認識して、JANコードであればその内容を取得するというシステムにするということです。

連続的に読み取りができる

これは要するに、一回目のバーコード読み取りの後に、すぐに二回目のバーコード読み取りを可能にするということです。三回目以降も同じです。ただし、同じバーコードを連続して読み取ってもしょうがないので、最後に読み取ったバーコードと違う番号のバーコードである場合に限って、その後の処理をさせるようなプログラムに仕上げます。今回は、バーコードを読み取った場合に、トーストを表示させるようにしていきます。

カメラ画面をカスタマイズする

バーコードの読み取り画面には、当然ですがカメラを使って、その映像を映している部分があります。デフォルトの設定だと、画面いっぱいにカメラ画面が広がってしまうので、同じカメラ画面にも、カメラ映像部分を自由に配置したり、テキストを表示させたり、ボタンの配置をしたりなど、UIを加えていこうと思います。

アプリの作成

使用するライブラリ

今回の開発に使用するライブラリは、ZXingというライブラリです。これで、「ゼブラクロッシング」と読むそうです。
まずは、このライブラリをインストールするところから始まります。以下に、ZXingのライブラリの元を置いておくので、興味のある方はご覧になってください。
github.com
それでは、実際にインストールしていきます。以下に示したコードをbuild.gradle(:app)に記入します。dependenciesはすでに書かれていると思うので、中括弧内を記入するようにしてください。

dependencies {
    implementation 'com.journeyapps:zxing-android-embedded:3.0.2@aar'
    implementation 'com.google.zxing:core:3.2.0'
}

記入したら、「Gradleファイルが変更されましたよ」的なメッセージとともに、右上付近に「Sync now」が出てくると思うので、それをクリックしてください。
これでインストールは完了です。簡単にできてしまいますね。続いて、UIの配置をしていきます。

レイアウトの作成

今回は、起動したらバーコード読み取り画面が開き、バーコード読み取ることでテキストボックスに読み取った内容を表示するような設計にしたいと思います。以下が、そのソースコードになります。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.journeyapps.barcodescanner.CompoundBarcodeView
        android:id="@+id/barcodeView"
        android:layout_width="340dp"
        android:layout_height="160dp"
        android:layout_marginTop="40dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </com.journeyapps.barcodescanner.CompoundBarcodeView>

    <TextView
        android:id="@+id/getNumber"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="60dp"
        android:text="Hello World!"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/barcodeView" />

</androidx.constraintlayout.widget.ConstraintLayout>

このソースコードを入力した際の、ブループリントが以下のスクショです。

f:id:PsyTouSan:20211107212441p:plain:w400
カメラ画面のブループリント

見てもらうと分かる通り、バーコードリーダのカメラ画面は、com.journeyapps.barcodescanner.CompoundBarcodeView で配置できるようになります。あとは、layout_width や layout_height などで、サイズを決めたり、android:id などでidを付けたりします。この辺りは、他のオブジェクトと変わりありません。
続いて、Javaを用いてプログラムを書いていきます。

Javaプログラムの作成

とりあえず、ソースコードを張り付けておきます。

package com.samplegame.barcode_sample;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Camera;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.ResultPoint;
import com.journeyapps.barcodescanner.BarcodeCallback;
import com.journeyapps.barcodescanner.BarcodeResult;
import com.journeyapps.barcodescanner.CompoundBarcodeView;
import com.journeyapps.barcodescanner.camera.CameraSettings;

import java.util.List;

public class MainActivity extends AppCompatActivity {
    CompoundBarcodeView barcodeView;
    private String lastResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if(ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
            String[] permissions = {Manifest.permission.CAMERA};
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 100);
            return;
        }
        CameraSetting();
        readBarcode();
    }

    private void CameraSetting(){
        barcodeView = findViewById(R.id.barcodeView);
        CameraSettings settings = barcodeView.getBarcodeView().getCameraSettings();
        barcodeView.getBarcodeView().setCameraSettings(settings);
        barcodeView.setStatusText("バーコードが読めます");
        barcodeView.resume();
        readBarcode();
    }
    private void readBarcode(){
        barcodeView.decodeContinuous(new BarcodeCallback() {
            final TextView getNumber = findViewById(R.id.getNumber);
            @Override
            public void barcodeResult(BarcodeResult result) {
                //このif文で、不必要な連続読みを防ぐ
                if (result.getText() == null || result.getText().equals(lastResult)){
                    return;
                }
                //このif文で、読み取られたバーコードがJANコードかどうか判定する
                if (result.getBarcodeFormat() != BarcodeFormat.EAN_13){
                    return;
                }
                lastResult = result.getText();
                Toast.makeText(MainActivity.this, "読み取りました", Toast.LENGTH_LONG).show();
                getNumber.setText(result.getText());
            }

            @Override
            public void possibleResultPoints(List<ResultPoint> resultPoints) {

            }
        });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 100 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
                return;
            }
        }
        CameraSetting();
    }
}

ざっとこんな感じです。

カメラ画面のカスタマイズ

よくある方法としては、IntentIntegratorクラスのinitiateScanメソッドを使用する方法があります。ところが、この方法では画面いっぱいにカメラが広がったアクティビティが立ち上がってしまいます。これでは、カメラ画面のカスタマイズという最初に掲げた目標からそれてしまうので、今回はCameraSettingクラスを使用する方法で実装しました。

アクセス許可を尋ねるダイアログの表示

まず、アプリの起動時にカメラの使用が許可されているかをチェックします。その判定を行っているのが、以下に示したコードです。

        if(ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
            String[] permissions = {Manifest.permission.CAMERA};
            ActivityCompat.requestPermissions(MainActivity.this, permissions, 100);
            return;
        }

これをonCreate内に記述しています。もし、カメラの使用許可が下りていない場合は、ダイアログを表示します。そのダイアログの表示を担っているのが、以下のコードです。

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 100 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
                return;
            }
        }
        CameraSetting();
    }

onRequestPermissionResultメソッドを継承しています。ここでもう一度、許可が下りているかどうか判定を行っています。こうして、ユーザーからカメラの使用を許可された場合に、CameraSettingメソッドを実行しています。もし拒否された場合は、アプリは起動したままですが、カメラには何も映りません。

f:id:PsyTouSan:20211114165632j:plain:w220
アクセス許可のダイアログ
JANコードのみを認識するようにする

バーコードがJANコードなのかどうかを判定するには、BarcodeFormat という列挙クラスを使うと都合が良さそうだったので、これを使用しました。以下のスクショは、裏表紙のISBNを読み取った時のものです。

f:id:PsyTouSan:20211114165031j:plain:w220
実際に読み取った様子

試しに、当ブログのURLをQRコードに変換したものを撮影してみましたが、反応していないのがわかります。

f:id:PsyTouSan:20211114174530j:plain:w220
QRコードには反応しない
連続的に読み取りができる

最後に読み取ったバーコードと違う番号を認識した場合に、テキストビューに新たに読み取った方の番号を表示します。これは、decodeContinuousメソッドで実装できます。仮に、一回のみ読み取りたい場合は、decodeSingleメソッドを用いるといいでしょう。

まとめ

まとめると、

  • カメラ画面はCameraSettingクラスを用いることでカスタマイズ可能
  • アクセス許可ダイアログは、標準ライブラリであるonRequestPermissionResultメソッドを実装すれば割と簡単に作れる
  • 認識したバーコードは、BarcodeFormaから取得できる
  • 連続的な読み取りはdecodeContinuousメソッドを用いる

といった感じです。
出来るだけわかりやすくなるように解説したつもりでしたが、私自身、能力不足を感じる部分もありますので、説明が下手になっている部分もあると思いますが、参考になれば幸いです。

それではまた。

【Java】ISBNから書籍情報を取得するAndroidアプリを作成しました

この記事は、群馬高専Advent Calendaの7日目の記事です。
adventar.org


非同期通信処理を用いて、HTTP通信処理を実装していきます。また、今回ではHTTP通信をする際に、Google Books APIを使用して実際にインターネット接続を経由して、書籍の情報を入手してみたいと思います。

今回作るもの

インターネット経由で情報を取得するという、考えるだけでは簡単なアプリを作りますが、一応最終的な完成品を以下に示しておきたいと思います。

f:id:PsyTouSan:20211202134031j:plain:w250
作るもの(ISBNからタイトルと著者を表示する)
Google Books API とは

Google Books APIは、特定のURLに書籍の裏表紙に記されているISBNをクエリパラメータとしてアクセスすることで、そのISBNの情報から、書籍のタイトル、著者、出版社、説明、などの情報を含んだJSONデータを返してくれるAPIです。特別な申請などはしなくても問題なく、誰でも無料で扱うことができるので、今回はこのAPIを活用したいと思います。試しに、以下のURLを踏んでみてください。

https://www.googleapis.com/books/v1/volumes?q=9784065163870
JSONデータが返ってくると思います。

非同期通信処理の準備

そもそも非同期通信処理とは

まず非同期通信処理について簡単に説明します。理解しているという人は飛ばしてもらっても構いません。
HTTP接続や、ダウンロードなどの時間のかかる処理を、メインで処理を実行するスレッド(メインスレッド)に任せてしまうと、それ以降の処理が滞ってしまうので、これは避けるべきことです。そこで、時間のかかる処理を、メインスレッドではない新たなスレッド(サブスレッド)に実行させることで、時間のかかる処理をしている間に、別の処理を実行できるので、効率的かつ安全であるということです。

f:id:PsyTouSan:20211120210929p:plain
非同期通信処理の簡略図

処理の記述

ここからが本題です。Androidアプリ内で、非同期通信をしていきます。とはいっても、いきなりソースコードを示すと分かりにくくなりそうなので、手順だけ先に示しておきます。

  1. looperとhandlerを宣言する
  2. 時間のかかる処理をするのクラスを作成し、インスタンス化する
  3. サブスレッドを用意する
  4. サブスレッドで処理を実行する

と言った感じです。結構長いコードを書くことになります。

マニフェストの設定

このアプリケーションは、デバイスにインターネットの接続を必要とします。なので、インターネット接続をさせるために、AndroidManifestを少しいじります。
以下のコードを参考に、一行だけ追加してください。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=...省略

    //以下の一行を記述
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        ...省略
</manifest>

レイアウトの作成

レイアウトはこんな感じです。

f:id:PsyTouSan:20211127184733p:plain:w450
レイアウトとブループリント

ソースコードは以下の通りです。今回は、簡単なアプリを作るだけですし、何かと面倒だったのでstrings.xmlファイルは使用しませんでした。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editIsbn"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="120dp"
        android:inputType="number"
        android:hint="ISBNを入力してください"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/search_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:hint="検索"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editIsbn" />

    <TextView
        android:id="@+id/resultTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:textSize="25sp"
        android:text="Hello World!"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/search_button" />

    <TextView
        android:id="@+id/resultAuthor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:text="Hello Android!"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/resultTitle" />

</androidx.constraintlayout.widget.ConstraintLayout>

簡単に説明しますと、テキストボックスにISBNが入力されて「検索」ボタンが押下されると、その下にあるテキストビューにその書籍のタイトルと著者が表示されるといった仕組みです。
その他、EditText内のinputTypeで、表示されるキーボードと、そのテキストボックスに入力可能な文字が指定できます。今回の場合は、numberを指定しているので、テキストボックスをタップしたときに表示されるキーボードは、一般的な日本語配列キーボードではなく、数字や記号が打てるキーボードが立ち上がり、さらに数字以外の文字の入力が不可能になります。そのほかにも、ここのnumberのところを「textPassword」と指定すれば、入力時に文字が見えないように「・」で隠してくれたり、「phone」とすれば、電話番号を入力しやすいテンキーが立ち上がるようになったりします。

Javaプログラムの作成

とりあえずコメント付きのソースコードを張り付けておきます。ソースコードの後に、いろいろな説明を書いてありますが、onCreateメソッドの役割だったり、クリック処理の記述方法など、非同期に関する以外の処理の説明は省かせていただきます。

package com.example.multithread_sample;

import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.os.HandlerCompat;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {
    private static final String GoogleBooksAPI = "https://www.googleapis.com/books/v1/volumes?q=";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        EditText isbn = findViewById(R.id.editIsbn);
        Button search = findViewById(R.id.search_button);
        search.setOnClickListener(v -> {
            try {
                if (isbn.getText().toString().length() != 13){
                    Toast.makeText(MainActivity.this, "適切な値を入れてください", Toast.LENGTH_LONG).show();
                } else{
                    BookSearch(isbn.getText().toString());
                }
            } catch (NullPointerException e){
                Log.d("Null Exception","EditText is null");
                Toast.makeText(MainActivity.this, "適切な値を入れてください", Toast.LENGTH_LONG).show();
            }

        });
    }

    //メインスレッドでの動作を保証するアノテーション
    @UiThread
    private void BookSearch(final String Isbn) {
        String urlFull = GoogleBooksAPI + Isbn;
        Looper mainLooper = Looper.getMainLooper();
        Handler handler = HandlerCompat.createAsync(mainLooper);
        BookDataReceiver receiver = new BookDataReceiver(handler, urlFull);
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(receiver);
        executorService.shutdown();

    }

    private class BookDataReceiver implements Runnable {
        private final Handler _handler;
        private final String _urlFull;

        private BookDataReceiver(Handler handler, String urlFull) {
            _handler = handler;
            _urlFull = urlFull;
        }

        //サブスレッドでの動作することを保証するアノテーション
        @WorkerThread
        @Override
        public void run() {
            //ここに非同期処理を記述している
            //HTTP通信するオブジェクトの作成
            HttpURLConnection connection = null;
            //連続的なデータを格納するオブジェクト
            InputStream stream = null;
            //取得したJSONデータを格納する
            String result = "";
            try {
                URL url = new URL(_urlFull);
                //接続用のオブジェクトを生成する
                connection = (HttpURLConnection) url.openConnection();
                //接続のタイムアウトを指定する
                connection.setConnectTimeout(1000);
                //値を取得するタイムアウトを時間を指定
                connection.setReadTimeout(1000);
                //値を送信する場合はPOST、取得する場合はGETを指定する
                connection.setRequestMethod("GET");
                //接続を行う
                connection.connect();
                //streamに取得した値を格納する
                stream = connection.getInputStream();
                //129行目に示したinputStream関数を用いてString型のデータに変換
                result = inputStream(stream);
            } catch (MalformedURLException e) {
                Toast.makeText(MainActivity.this,"URLの変換に失敗", Toast.LENGTH_LONG).show();
            } catch (SocketTimeoutException e) {
                Toast.makeText(MainActivity.this, "接続タイムアウト", Toast.LENGTH_LONG).show();
            } catch (IOException e) {
                Toast.makeText(MainActivity.this, "通信できませんでした",Toast.LENGTH_LONG).show();
            } finally {
                if (connection != null){
                    //切断する
                    connection.disconnect();
                }
                if (stream != null){
                    try {
                        stream.close();
                    } catch (IOException e){
                        Toast.makeText(MainActivity.this, "解放失敗", Toast.LENGTH_LONG).show();
                    }
                }
            }
            JsonDecoder decoder = new JsonDecoder(result);
            _handler.post(decoder);
        }

        private String inputStream(InputStream stream) throws IOException {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
            StringBuffer result = new StringBuffer();
            char[] buffer = new char[1024];
            int line;
            while (0 <= (line = reader.read(buffer))) {
                result.append(buffer, 0, line);
            }
            return result.toString();
        }
    }

    private class JsonDecoder implements Runnable {
        private final String _result;

        private JsonDecoder(String result) {
            _result = result;
        }

        @UiThread
        @Override
        public void run() {
            String title = "";
            String author = "";
            try {
                //JSONオブジェクトを生成
                JSONObject jsonObject = new JSONObject(_result);
                //取得したJSONデータから「items」と名づけられた配列を取得する
                JSONArray items = jsonObject.getJSONArray("items");
                //取得した「items」配列のゼロ番目の要素を取得
                JSONObject itemValue = items.getJSONObject(0);
                //さらに、itemValue内の「volumeInfo」と名付けられた配列を取得
                JSONObject info = itemValue.getJSONObject("volumeInfo");
                //タイトルを取得
                title = info.getString("title");
                //「authors」と名付けられた配列を取得
                JSONArray authors = info.getJSONArray("authors");
                //著者を取得
                author = authors.getString(0);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            TextView resultTitle = findViewById(R.id.resultTitle);
            TextView resultAuthor = findViewById(R.id.resultAuthor);
            resultTitle.setText("タイトル:" + title);
            resultAuthor.setText("著者:" + author);
        }
    }
}

Looper

Looperは、自身の属したスレッドのMessageQueueに格納された処理を、先入先出方式で実行し、これを無限にループします。MessageQueueとは、処理を順番に実行する箱のようなものです。メインスレッドは、このMessageQueueというものを最初から保持しています。

Handler

Handlerを用いることで、別スレッドで実行させたい処理をMessageQueueに追加できます。この別スレッドというのは、Looperを持っているスレッドのことです。これにより、HandlerからLooperを通じて、スレッド間の通信を可能にします。

CreateAsync

これだけの説明では、HandlerとLooperに関する説明としては不十分です。しかし、62行目に示したCreateAsyncメソッドを用いることで、この複雑な仕組みを簡略化して実装することができます。62行目では、HandlerをCreateAsyncメソッドにメインメソッドのLooperを渡すことで生成しています。こうすることで、「メインメソッドのLooperの持つQueueに、処理を追加できるHandler」が完成するわけです。これにより、非同期で実行させたい処理をさせることもできれば、後述する「post」を用いた、メインスレッドに戻って処理を実行させる(厳密には、getMainLooperを実行したスレッドに戻って実行させる)ということも可能になります。
LooperとHandlerに関する詳しい解説は、以下のリンクを参考にさせていただきましたので、掲載させていただきます。

LooperとHandlerに関して参考になったサイト
academy.realm.io
developer.android.com

サブスレッドに実行させる処理

さて、次はGoogle Books APIを用いた書籍情報の取得をを行うクラスの作成をしていきたいと思います。ここで扱う内容は以下の通りです。

  1. HTTP接続を行う
  2. 返されたJSONデータを文字列(バッファ)として取得する
  3. バッファからお望みのデータを抽出する
  4. 画面に表示する

上に示した番号の、2と3の部分で書籍を取得しています。まず2の部分で、帰ってきた文字列データをバッファとして格納します。そして、3の部分でそのバッファ内にある文字列から必要な情報を抽出するといった感じです。

HTTP接続の開始

HTTP接続はサブスレッドで実行するため、Runnableインターフェースの中にある、runメソッドの中に記述しています。Runnableインターフェースは、runメソッドを必ず表記する必要があります。ちなみに、runメソッドは、引数も戻り値もありません。このrunメソッドは、Runnableインターフェースを実装したスレッドが開始されたときに、自動的に呼び出されます。したがって、ボタンを押下したときにサブスレッドを開始することで、runメソッド内の処理が実行されることになります。HTTP接続を行う処理の記述は、例外処理の中に入れています。

JSONデータをバッファとして取得

HTTP経由でJSONデータを取得します。129行目のメソッドでJSONデータをString型に変換しています。以下は、そのメソッドの抜粋です。

 private String inputStream(InputStream stream) throws IOException {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
            StringBuffer result = new StringBuffer();
            char[] buffer = new char[1024];
            int line;
            while (0 <= (line = reader.read(buffer))) {
                result.append(buffer, 0, line);
            }
            return result.toString();
        }

望みのデータを取得する

String型に変換したJSON生データから、タイトルと著者を取得します。以下は、JSON生データから、必要な値を取得するメソッドの抜粋です。

 private class JsonDecoder implements Runnable {
        private final String _result;

        private JsonDecoder(String result) {
            _result = result;
        }

        @UiThread
        @Override
        public void run() {
            String title = "";
            String author = "";
            try {
                //JSONオブジェクトを生成
                JSONObject jsonObject = new JSONObject(_result);
                //取得したJSONデータから「items」と名づけられた配列を取得する
                JSONArray items = jsonObject.getJSONArray("items");
                //取得した「items」配列のゼロ番目の要素を取得
                JSONObject itemValue = items.getJSONObject(0);
                //さらに、itemValue内の「volumeInfo」と名付けられた配列を取得
                JSONObject info = itemValue.getJSONObject("volumeInfo");
                //タイトルを取得
                title = info.getString("title");
                //「authors」と名付けられた配列を取得
                JSONArray authors = info.getJSONArray("authors");
                //著者を取得
                author = authors.getString(0);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            TextView resultTitle = findViewById(R.id.resultTitle);
            TextView resultAuthor = findViewById(R.id.resultAuthor);
            resultTitle.setText("タイトル:" + title);
            resultAuthor.setText("著者:" + author);
        }

このメソッドは、メインスレッドで実行しています。なので、アノテーションでも@UIThreadを指定しています。非同期通信を行うときと同様、Runnableインターフェースを実装して、その中のrunメソッド内に処理を記述しています。サブスレッドからメインスレッドに処理を戻す場合でも、非同期処理を行うときと同じように、runメソッド内に処理を記述します。

JSON生データからデータを抽出する

このrunメソッド内の処理を簡単に説明します。まず、前段階において、テキストボックスに入力されたISBNから、Google Books API を用いて、HTTP接続によりJSONの生データをString型で受け取ることを行いました。今度は、そのString型データから、書籍のタイトルと著者の情報を引き出します。とりあえず、実際のJSONデータを見てみます。以下のリンクにアクセスすると、JSONデータを取得できるのがわかると思います。

https://www.googleapis.com/books/v1/volumes?q=9784065163870

このデータに基づいて、JSONオブジェクトを取得したり、JSON配列を取得したり、JSON配列内の要素を抽出したりしています。先ほどの非同期処理では、このデータを取得していたということになります。
(余談ですが、この書籍はFlutter開発に関する書籍です。AndroidStudioで開発でき、Androidだけでなく、iOSアプリやWebアプリも作れるので、近年注目が集まっています。そのうち、これに関する記事も出していきたいと思っています…)
基本的には、ソースコードのコメント通りではありますが、簡潔にまとめると

  • JSONObjectクラスをnewし、そこにStringを渡して、JSONオブジェクトを生成
  • JSONArrayメソッドに、Stringを渡して、その名前の配列を取得
  • JSONObjectクラスののgetStringメソッドへStringを渡して、その名前の要素を取得

という感じになり、基本的にこれらでデータを抽出できます。

画面に表示する

レイアウト画面のタイトル、著者欄に文字を表示させます。上に示したソースコードの31から34行目に記述された内容が、文字の表示を担っています。
非同期処理の記述に比べたら遥かに簡単ですね。

動作確認

さて、ここまで記述が終えたので動作させてみます。

f:id:PsyTouSan:20211202133759j:plain:w250
起動直後の画面の様子

このテキストボックスに適当にISBNを入力してみてください。入力が終えたら、検索ボタンをタップしてください。HelloWorldとHelloAndroidと書かれている文章がタイトルと著者に代わるはずです。

f:id:PsyTouSan:20211202134031j:plain:w250
検索結果

まとめ

今回の記事の内容をまとめると

  • HTTP接続は基本的に非同期処理で行う
  • Androidの非同期処理には、LooperとHandlerが必要
  • スレッドの開始時にRunnable内のrunメソッドが実行される
  • runメソッド内に、非同期処理、もしくはサブからメインスレッドに戻ってきたときの処理を記述する
  • AndroidでもJSONデータを扱うことができる

といった感じになります。

非同期処理を用いて、ISBNから書籍データを取得することに成功しました。今回は、Google Books API を利用した方法でしたが、ほかにも、天気の情報を取得するとか、ニュースを取得するなどの他社のAPIサービスを利用すれば、応用の幅が一気に広がるので、ぜひ活用していきたいですね。

以上です。

Arduinoで複数のジャイロセンサとSPI通信

概要

今回は、MPU9250を複数用いてArduinoとSPI通信していきたいと思います。具体的には、SPI通信によってI2Cでは不可能なMPU9250の複数制御を試したので、その記録をここに残します。複数と言っても、ここでは二つのジャイロセンサで実践します。でも、この方法であれば二つだけではなく、三つでも四つでもほぼ同時に通信が可能です。そのあたりの説明は後程説明します。
使用する部品は以下のものです。

f:id:PsyTouSan:20210925090655j:plain
MPU9250

こちらがMPU9250です。3軸加速度、3軸角速度、3軸方位角の計9軸を測定することができるIMUで、コンパクトにまとまっており、価格も数百円程度と安価に入手できます。

MPU9250の仕様

MPU9250は主に、I2C通信とSPI通信の二種類の通信形態をサポートしています。特に重要なのは以下の二つです。

  • SPI通信は、加速度センサとジャイロセンサのみサポート
  • それぞれのデータは上位7ビットと下位7ビットにアドレスが分けられている

特に、一つ目の仕様に関しては致命的なレベルです。要するに、SPI通信をする際は、地磁気センサの使用ができない(頑張ればできるかもしれない)ということになります。なので、今回は加速度と角加速度の計6軸の出力で実装したいと思います。

詳しいことは、以下のデータシートを参考にしてください。

https://invensense.tdk.com/wp-content/uploads/2015/02/PS-MPU-9250A-01-v1.1.pdf

SPI通信を選んだ理由

というのも、複数のセンサを扱う場合I2C通信では不都合なことが発生します。

以下は、I2C通信の仕組みを簡略化した図です。

f:id:PsyTouSan:20210929145302p:plain
I2C通信の簡単な仕組み


上の図のSDAが、送受信するデータですが、このうちの青枠がマスターからスレーブ、黄枠がスレーブからマスターに送信されるデータです。

このうちの最初の7ビットは、通信を開始したいスレーブのアドレスを持っています。

要するに、最初の7ビットで、どのスレーブとやりとりするかを決定します。次のビットで、書き込みをするか、読み込みをするかを送ります。この時、書き込みをする場合は1が送信されます。すると、このアドレスと一致したスレーブから応答を得ることができます。それが、ACKです。ただ、ここで一つ問題が発生します。複数の同じスレーブ(今回の場合はMPU9250)と通信をする場合、互いにスレーブアドレスが一致してしまうため、応答が衝突してしまい、うまく動作しないということです。

一応、上記のデータシートによれば、MPU9250は二種類のスレーブアドレスを切り替えることができるみたいです。これが意味することは、I2C通信で二つのMPU9250と通信ができるということです。しかし、MAXで二種類のアドレスしか持つことができないため、I2C通信では三つ以上の同じセンサを扱うことができません。しかし、SPI通信であれば、SSピンのデジタル信号を切り替える(通信したい相手のみをLOWに保つ)ことで、選択的に通信することができます。

f:id:PsyTouSan:20211001135454p:plain
SPI通信

なので、SSピンのHIGHとLOWを切り替えて、切り替えるたびにMISOとMOSIを読み取るということをすれば、複数のセンサと同時に通信することができるというわけです。

Arduinoで実装する

さて、実際にコードを書いていきます。使用するのはArduino MEGA 2560です。
それ以外のArduinoでも問題ありませんが、MEGAとはピン配置や番号が異なっているため、注意が必要です。今回は、手元にあるArduino MEGA 2560を使用して実装していきます。また、細かい説明に関しては、後ほど説明します。
接続は次のとおりです

f:id:PsyTouSan:20210929210802p:plain
配線方法

こんな感じです。NCS以外は並列につないでいます。ちなみにVccは3.3Vです。
上の図は、三つがつ結線されていますが、いくつで行っても問題ありません。これよりも多くつなぐ場合でも、NCSピン以外を並列になるようにつないで、NCSピンだけを他のデジタルピンにつなげば大丈夫です。ただし、あまり多く結線してしまうと、それだけ処理がArduinoへの負担も大きくなりますから、そこは注意ですね。
今回は、二つで行ってみたいと思います。

f:id:PsyTouSan:20211002090943j:plain
結線の様子

少しわかりにくいですが、実際につないでみた写真です。

それでは、Arduino IDEを用いてプログラムを記述していきたいと思います。
以下に示すのはソースコードです。

#include <SPI.h>

int data[6];
int GX,GY,GZ; //角速度を格納
int AX,AY,AZ; //加速度を格納
float ax,ay,az,gx,gy,gz;

void setup() {
  pinMode(49,OUTPUT);
  pinMode(48,OUTPUT);
  SPI.setBitOrder(MSBFIRST);
  SPI.setClockDivider(SPI_CLOCK_DIV4);
  SPI.setDataMode(SPI_MODE3);
  SPI.begin();  
  
  Serial.begin(2000000);
}

void loop() {
  dataRead(49,data);   //ssピン49のセンサの六つの値を取得
  AX = data[0];
  AY = data[1];
  AZ = data[2];
  GX = data[3];
  GY = data[4];
  GZ = data[5];

  Serial.print("A:");
  Serial.print(GX); Serial.print(",");
  Serial.print(GY); Serial.print(",");
  Serial.print(GZ); Serial.print(",");

  dataRead(48,data);   //ssピン48のセンサの六つの値を取得
  AX = data[0];
  AY = data[1];
  AZ = data[2];
  GX = data[3];
  GY = data[4];
  GZ = data[5];

  Serial.print("B:");
  Serial.print(GX); Serial.print(",");
  Serial.print(GY); Serial.print(",");
  Serial.print(GZ); Serial.println("");
}

//加速度センサの測定値を取得するだけの関数
int dataRead(int ss,int data[6])
//第一引数から、読み取るセンサのcsピン番号,読み取りデータ格納用の配列
{
  byte dataH = Read(ss,0x3B);
  byte dataL = Read(ss,0x3C);
  ax = dataH << 8 | dataL;
  data[0] = ax;

  dataH = Read(ss,0x3D);
  dataL = Read(ss,0x3E);
  ay = dataH << 8 | dataL;
  data[1] = ay;
  
  dataH = Read(ss,0x3F);
  dataL = Read(ss,0x40);
  az = dataH << 8 | dataL;
  data[2] = az;

  dataH = Read(ss,0x43);
  dataL = Read(ss,0x44);
  gx = dataH << 8 | dataL;
  data[3] = gx;
  
  dataH = Read(ss,0x45);
  dataL = Read(ss,0x46);
  gy = dataH << 8 | dataL;
  data[4] = gy;
  
  dataH = Read(ss,0x47);
  dataL = Read(ss,0x48);
  gz = dataH << 8 | dataL;
  data[5] = gz;

  return 0;
  
}

//ピンを読み込む関数
byte Read(int ss,byte address)
//第一引数から、読み取るセンサのcsピン番号,アドレス
{
  digitalWrite(ss,LOW);
  SPI.transfer(address | 0x80);
  byte data = SPI.transfer(0);
  digitalWrite(ss,HIGH);
  return data;
}

コードの簡単な説明

SPIライブラリ

ArduinoIDEには、SPI通信をするのに必要なインターフェースを簡単に揃えることができるライブラリがあります。それが、SPI.hライブラリです。
11行目から14行目までは、SPI通信をする際に必要な設定をしています。

//最上位ビットを転送する
  SPI.setBitOrder(MSBFIRST);
//SPI通信の動作スピード
  SPI.setClockDivider(SPI_CLOCK_DIV4);
//SPI通信の動作モード
  SPI.setDataMode(SPI_MODE3);
//SPI通信を開始する
  SPI.begin();  
転送する順序

SPI.BitOrder()関数は、ビットの送受信する順番を「最上位ビットを最初に送る」のか、「最下位ビットを最初に送る」のかを決定する。今回の場合は、MSBFIRSTとすることで、最上位ビットを先に転送している。

動作スピード

SPI.setClockDriver()関数で、システムのクロックの速さに応じたクロックを設定できます。要するに、システムクロックが32MHzの時に、SPI_CLOCK_DIV4とすれば、32MHzを四分割した8MHzとなりますし、SPI_CLOCK_DIV8とすれば、八分割の4MHzとなります。分割数は、最後の数字を変えることで変化できますが、「2、4、8、16、32、64、128」の7つに限定されています。

動作モード

SPI.setDataMode()関数で、SPI通信の動作モードを選択できますが、SPI_MODE0、SPI_MODE1、SPI_MODE2、SPI_MODE3の4つしかない。この4つはそれぞれ極性モードと送受信するタイミングが異なる。詳しい内容はここでは割愛する。

ジャイロデータの取得

まず通信する相手を選択

今回の場合、2つのMPU9250を使用しましたが、前述したとおりどちらか一方のSSピンをHIGHに保つことでそのセンサとの通信を遮断することが必要になってきます。それを行っている関数が、86行目に示したbyte Read関数です。この関数が呼び出された時、その第一引数に渡された番号をLOWに変化させ、SPI通信のトランザクションを開始します。そして、それが終了したらすぐにHIGHに戻します。

次いでデータの取得

ソースコードの51行目から54行目を例に見てみます。以下はその部分の切り取りです。

  byte dataH = Read(ss,0x3B);
  byte dataL = Read(ss,0x3C);
  ax = dataH << 8 | dataL;
  data[0] = ax;

MPU9250から転送されてくるジャイロデータは、上位8ビットと下位8ビットに分かれて送られてきます。なので、それらをdataHとdataLの2つの変数に分けて受け取っています。それらをビットシフトすることで、ジャイロデータを取得していることになります。

結果

以下のスクショにはセンサAとセンサBの3軸角速度をそれぞれ表示しています。(加速度も表示してしまうと、ごちゃごちゃしてよくわからなくなってしまうので。)

f:id:PsyTouSan:20211003223438p:plain
取得した結果

センサAのみを軽く振動させている状態です。生値のままですが、実際に2つのジャイロセンサの値を取得出来ていることがわかります。
以上です。