【Android、Bluetooth備忘録】notifyCharacteristicChangedで、201エラーがでた場合

サンプルコードは、すべてGNU GPL2を想定しています。

最初の対処法としては、201エラーがでたとき、数回リトライするというものでした。(動きません。

Java
        @Override
        public boolean transferData(@NonNull byte[] writeBuffer) throws SecurityException {
            try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    int result = gattServer.notifyCharacteristicChanged(bluetoothDevice, midiCharacteristic, false, writeBuffer);
                    if (MidiNet.isDebug) {
                        Log.e(TAG, "notify " + result + " " + MXUtil.dumpHex(writeBuffer));
                    }
                    /*
                    if (result == 201) {
                        int retry = 0;
                        do {
                            try {
                                Thread.sleep(20);
                            }catch (InterruptedException ex) {
                            }
                            ++ retry;
                            if (retry >= 2) {
                                //Log.e(Constants.TAG, "201 retry " +  retry);
                            }
                            if (retry >= 100) {
                                break;
                            }
                            result = gattServer.notifyCharacteristicChanged(bluetoothDevice, midiCharacteristic, false, writeBuffer);
                            if (result != 0 && result != 201) {
                                if (isDebug) {
                                    Log.e(Constants.TAG, "*** " + result + " retry " +  retry);
                                }
                            }
                        }while(result == 201);
                    }
                    */
                    return result == BluetoothStatusCodes.SUCCESS;
                } else {
                    midiCharacteristic.setValue(writeBuffer);
                    boolean result = gattServer.notifyCharacteristicChanged(bluetoothDevice, midiCharacteristic, false);
                    if (MidiNet.isDebug) {
                        Log.e(TAG, "notify " + result + " " + MXUtil.dumpHex(writeBuffer));
                    }

                    /*

                    if (!result) {
                        int retry = 0;
                        do {
                            try {
                                Thread.sleep(20);
                            }catch (InterruptedException ex) {
                            }
                            ++ retry;
                            if (retry >= 2) {
                                if (isDebug) {
                                    Log.e(Constants.TAG, "201 retry " +  retry);
                                }
                            }
                            if (retry >= 100) {
                                break;
                            }
                            result = gattServer.notifyCharacteristicChanged(bluetoothDevice, midiCharacteristic, false);
                        }while(!result);
                    }
                     */
                    return result;
                }
            } catch (Throwable ignored) {
                // android.os.DeadObjectException will be thrown
                // ignore it
                return false;
            }

        }

waitではnotifyAllで起きてしまうため、Thread.sleepを入れてみたりしましたが、うまくいきません。

結果から申し上げます。送信および、BLEの待機スレッドを作成する必要があります。

MABThread.java 順番に処理するクラス

Java
package org.star_advance.mixandcc.mab;

import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import java.util.LinkedList;

import org.star_advance.mixandcc.midinet.MidiNet;
import org.star_advance.mixandcc.midinet.libs.MXQueue;

public class MABThread {
    static String TAG = "BluetoothThread";
    public MABThread() {
        launchThread();
    }

    public synchronized void launchThread() {
        if (_thread != null && _thread.isAlive() == false) {
            _thread = null;
        }
        if (_thread == null) {
            _thread = new Thread(() -> infinityLoop());
            _thread.setDaemon(true);
            _thread.start();
            if (_thread.isAlive() == false) {
                synchronized (this) {
                    try {
                        wait(100);
                    }catch(InterruptedException ex) {
                    }
                }
            }
        }
    }

    MXQueue<Runnable> _queue = new MXQueue<>();
    Thread _thread = null;
    Handler handler = new Handler(Looper.getMainLooper());
    void pushMain(Runnable run) {
        handler.post(run);
    }

    LinkedList<Runnable> _validDelay = new LinkedList<>();
    boolean _pausing = true;
    boolean _freezing = false;
    public void pauseTillResponse() {
        _pausing = true;
    }
    public void freeze(boolean flag) {
        if (flag) {
            _freezing = true;
        }
        else {
            _freezing = false;
            synchronized (this) {
                notifyAll();
            }
        }
    }
    public void caughtResponse() {
        _pausing = false;
        synchronized (this) {
            notifyAll();
        }
    }
    //cancelDelay
    public void pushDelay(Runnable run, long time) {
        synchronized (_validDelay) {
            _validDelay.add(run);
        }
        new Thread(() -> {
            try {
                Thread.sleep(time);
            }catch (InterruptedException ex) {
                Log.e(TAG, ex.getMessage(), ex);
            }
            synchronized (_validDelay) {
                if (_validDelay.contains(run) == false) {
                    return;
                }
                _validDelay.remove(run);
            }
            push(run);
        }).start();
    }

    public void push(Runnable run) {
        if (true) {
            _queue.push(run);
            if (MidiNet.isDebug) {
                Log.e(TAG, "pushed " + _queue.size());
            }
            launchThread();
        }else {
            synchronized(this) {
                run.run();
            }
        }
    }

    public void infinityLoop() {
        try {
            long prevtime = System.currentTimeMillis();
            synchronized (this) {
                notifyAll();
            }
            while(true) {
                Runnable run = _queue.pop();
                if (run == null) {
                    break;
                }
                if(_pausing || _freezing) {
                    //最大1秒待機する(正確な時間は未定)
                    synchronized (this) {
                        try {
                            wait(1000);
                        }catch (InterruptedException ex) {

                        }
                    }
                    _pausing = false;
                }
                try {
                    run.run();
                    if (MidiNet.isDebug) {
                        Log.e(TAG, "poped " + _queue.size());
                    }
                }catch (Throwable ex) {
                    Log.e(TAG, ex.getMessage(), ex);
                }
                if (true) {
                    long stepTime = System.currentTimeMillis();
                    long time1 = 7 - (stepTime - prevtime);
                    if (time1 < 2) {
                        time1 = 2;
                    }
                    else if (time1 >= 7) {
                        time1 = 7;
                    }
                    try {
                        Thread.sleep(time1); //don't interrupt / notify awake
                    } catch (Throwable ex) {

                    }
                    prevtime = stepTime;
                }
            }
        }finally {
            _thread = null;
        }
    }

}

MXQueue.java 順番まち行列クラス

Java
package org.star_advance.mixandcc.midinet.libs;

import java.util.LinkedList;

public class MXQueue<T> {

    LinkedList<T> _queue;
    boolean _quit;

    public MXQueue() {
        _queue = new LinkedList<T>();
        _quit = false;
    }

    public synchronized void push(T obj) {
        _queue.add(obj);
        notifyAll();
    }

    public synchronized boolean isEmpty() {
        return _queue.isEmpty();
    }
    
    public synchronized  int size() {
        return _queue.size();
    }

    public synchronized T popAndNoRemove() {
        while (true) {
            while (_queue.isEmpty() && !_quit) {
                try {
                    wait(100);
                } catch (InterruptedException ex) {
                    return null;
                }
            }
            if (!_queue.isEmpty()) {
                return _queue.peekFirst();
            }
            notifyAll();
            if (_quit) {
                return null;
            }
        }
    }

    public synchronized T pop() {
        while (true) {
            while (_queue.isEmpty() && !_quit) {
                try {
                    wait(100);
                } catch (InterruptedException ex) {
                    return null;
                }
            }
            if (!_queue.isEmpty()) {
                return _queue.removeFirst();
            }
            notifyAll();
            if (_quit) {
                return null;
            }
        }
    }

    public synchronized void back(T item) {
        _queue.add(0, item);
        notifyAll();
    }

    public synchronized void quit() {
        _quit = true;
        notifyAll();
    }
}

BluetoothGattCallbackを派生するクラスをconnectGattで呼ぶと思いますが、その中で、onServiceDiscoverdなどでは、pauseTillResponseと、caughtResponseと用いて、シーケンスのように、レスポンスを待機してから次のタスクを実行します。

Java

    @Override
    public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
        if (MidiNet.isDebug) {
            Log.e(TAG, "onServicesDiscovered " + status);
        }
        _workDiscover100 = 200;
        if (gatt != null) {
            _device._gatt = gatt;
        }
        if (status != BluetoothGatt.GATT_SUCCESS) {
            return;
        }

        BluetoothGattService deviceInformationService = BleMidiDeviceUtils.getDeviceInformationService(gatt);
        if (deviceInformationService != null) {
            final BluetoothGattCharacteristic manufacturerCharacteristic = BleMidiDeviceUtils.getManufacturerCharacteristic(deviceInformationService);
            if (manufacturerCharacteristic != null) {
                if (MidiNet.isDebug) {
                    Log.e(TAG, "read manufacturerCharacteristic");
                }
                _device.getOwner().getThread().push(() -> {
                    // this calls onCharacteristicRead after completed
                    _device.getOwner().getThread().pauseTillResponse();
                    gatt.readCharacteristic(manufacturerCharacteristic);
                });
            }

            final BluetoothGattCharacteristic modelCharacteristic = BleMidiDeviceUtils.getModelCharacteristic(deviceInformationService);
            if (modelCharacteristic != null) {
                if (MidiNet.isDebug) {
                    Log.e(TAG, "read modelCharacteristic");
                }
                _device.getOwner().getThread().push(() -> {
                    _device.getOwner().getThread().pauseTillResponse();
                    gatt.readCharacteristic(modelCharacteristic);
                });
            }  
        }

中略

Java

    @Override
    public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value, int status) {
        if (MidiNet.isDebug) {
            Log.e(TAG, "onCharacteristicRead 2");
        }
        try {
            if (BleUuidUtils.matches(characteristic.getUuid(), BleMidiDeviceUtils.CHARACTERISTIC_MANUFACTURER_NAME) && value != null && value.length > 0) {
                String manufacturer = new String(value);

                _device._manufacture = manufacturer;
                if (MidiNet.isDebug) {
                    Log.e(TAG, "manufacture = " + manufacturer);
                }
            }
        } catch (Throwable ex) {
            Log.e(TAG, ex.getMessage(), ex);
        }
        try {
            if (BleUuidUtils.matches(characteristic.getUuid(), BleMidiDeviceUtils.CHARACTERISTIC_MODEL_NUMBER) && value != null && value.length > 0) {
                String model = new String(value);

                _device._model = model;
                if (MidiNet.isDebug) {
                    Log.e(TAG, "model = " + model);
                }
            }

        } catch (Throwable ex) {
            Log.e(TAG, ex.getMessage(), ex);
        }
        _device.getOwner().getThread().caughtResponse();
    }

これにより、すべてのBluetoothタスクが順次実行されるようになります。

データの処理も同様に、単一スレッドからおこないます。

Java
    public void dequeAndSend() {
        while (_queue.isEmpty() == false) {
            OneMessage packet0 = _queue.pop();
            if (packet0 == null) {
                continue;
            }
            if (packet0._data.length == 0) {
                continue;
            }

            if ((packet0._data[0] & 0xff) == 0xff) {
                int len = packet0._data.length;
                if (len >= 3) {
                    int type = packet0._data[1] & 0xff;
                    int len2 = packet0._data[2] & 0xff;
                    len -= 3;
                    if (len != len2) {
                        Log.e(TAG, "bad meta type " + type + " len " + len + " != " +len2);
                        continue;
                    }
                }
                else {
                    continue;
                }
            }

            boolean sysex1 = (packet0._data[0] & 0xff) == 0xf0;
            if (sysex1) {
                sendMidiSystemExclusive(packet0);
                stream.reset();
                break;
            }

            //packet0._time = 0;
            if (stream.size() == 0) {
                stream.write((byte) (0x80 | ((packet0._tick >> 7) & 0x3f)));
            }
            stream.write((byte) (0x80 | (packet0._tick & 0x7f)));
            stream.write(packet0._data, 0, packet0._data.length);

            while (!_queue.isEmpty()) {
                OneMessage packet = _queue.pop();
                boolean sysex2 = (packet._data[0] & 0xff) == 0xf0;
                if (sysex2) {
                    _queue.back(packet);
                    break;
                }
                if (stream.size() > 0 && stream.size() + packet._data.length + 2 >= getBufferSize()) {
                    _queue.back(packet);
                    break;
                }
                stream.write((byte) (0x80 | (packet._tick & 0x7f)));
                stream.write(packet._data, 0, packet._data.length);
            }

            byte[] data2 = stream.toByteArray();
            if (!transferData(data2)) {
                if (stream.size() <= getBufferSize()) {
                    try {
                        Thread.sleep(1);
                    } catch (Throwable ex) {

                    }
                    continue;
                }
            }

            stream.reset();
            break;//need thread sleep @ caller
        }
    }
    
    public void sendMidiMessage(OneMessage one) {
        startTransmitter();
        one._tick = getTimestamp();
        _queue.push(one);
        _mab.getThread().push(this::dequeAndSend);
    }

このように、転送したいパケットを、バッファーサイズにあわせて送信する関数を用意したあと、取りこぼしを発生させないため、MABThreadで、待機します。以下の部分です。if (true)は、falseでも動きますが、電波状況によっては、これがあったほうが安定します。

なおバッファーサイズは、MTUで指定されたサイズ(517?)より、BLEの基本32バイトを想定したほうがうまくいきます。

Java
                if (true) {
                    long stepTime = System.currentTimeMillis();
                    long time1 = 7 - (stepTime - prevtime);
                    if (time1 < 2) {
                        time1 = 2;
                    }
                    else if (time1 >= 7) {
                        time1 = 7;
                    }
                    try {
                        Thread.sleep(time1); //don't interrupt / notify awake
                    } catch (Throwable ex) {

                    }
                    prevtime = stepTime;
                }

以上です。これらのコードによって、まるで、RS-232Cのような処理が可能となります。

あとで、MIXandCCでなにができるか、動画も用意してこのページにさしこんでおきます。


Androidタブレット上の、MIXandCCより、スタンダードMIDIファイル再生し。BLE接続したiPhoneから送信する動画を用意したいです。

ところで、Android側をペリフェラルにするか、iOSがわをペリフェラルにするか選べますが、そのうち、iOSがペアリングをつよく要求するほうは使わないほうがいいです。これは重要です。

MACアドレスとパスコードで、ペアリングを記憶しますが、双方のデバイスとも、匿名のMacアドレスを用いています。個人のトラッキングがされたりして、プライバシーの問題を発生させないためだと思います。

結果として、MACアドレスがかわったタイミング(アプリや端末の再起動や再接続だとおもいます)以降、接続する際に、なんかしらの異常があります。以下の、コールバックについて、ステータスが0から0に変化したという通知がきて、まったくつながらなくなります。その場合、ペアリングを解除して、再度接続する必要があります。

ペアリングしなおしても、いいのですが、親と子を逆転させるなど、ペアリングを回避できる手段があれば、そのほうがいいです。

Java

    int _workDiscover100 = 0;
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) throws SecurityException {
        if (MidiNet.isDebug) {
            Log.e(TAG, "onConnectionStateChange " + status + " -> " + newState);
        }
        /*
        if (status == newState && status == 0) {
            BluetoothDevice ble = _device.getBluetoothDevice();
            if (ble != null) {
                if (true) {
                }
                else {
                    String seekName = ble.getName();
                    String seekAddr = ble.getAddress();
                    Set<BluetoothDevice> listAlready = _device.getOwner().getBluetoothAdapter().getBondedDevices();
                    if (listAlready != null && listAlready.size() > 0) {
                        int hit = 0;
                        int err = 0;
                        Log.e(TAG, "seek = " + seekName + " addr " + seekAddr);
                        for (BluetoothDevice already : listAlready) {
                            String name = already.getName();
                            String addr = already.getAddress();
                            Log.e(TAG, "bonded = " + name + " addr " + addr);
                            hit++;
                            if (already != ble && name.equals(seekName)) {
                                err++;
                            }
                        }
                        if (err >= 1) {
                            Log.e(TAG, "err = " + err + " handle " + _device.getOwner()._showError + " then " + _device.getOwner()._showErrorCount);
                            if (_device.getOwner()._showError != null) {
                                _device.getOwner()._showError.showError(_device.getOwner()._showErrorCount++);
                                _device.getOwner()._showError = null;
                            }
                            reconnect(_device);
                        }
                    }
                }
            }
        }*/
        if (gatt != null && _device._gatt != gatt) {
            if (_device._gatt == null) {
                _device._gatt = gatt;
            }
            else if (_device._gatt.getDevice() == gatt.getDevice()) {
                //gatt = _device._gatt;
                _device._gatt = gatt;
            }
            else if (_device._gatt.getDevice().getAddress().equals(gatt.getDevice().getAddress()) == true) {
                //gatt = _device._gatt ;
                _device._gatt = gatt;
            }
            else {
                Log.e(TAG, "fixed gatt 2 " + gatt + " != " + _device._gatt);
                _device._gatt = gatt;
            }
        }

        // In this method, the `status` parameter shall be ignored.
        // so, look `newState` parameter only.
        if ((status == 133 && newState == BluetoothProfile.STATE_DISCONNECTED)
            || (status == 0 && newState == 0)) {//both timeout
            if (true) {
                new Handler(Looper.getMainLooper()).post(() -> {
                    Toast.makeText(_connectContext, R.string.ble_cache_broken, Toast.LENGTH_LONG).show();
                });
            }
            else {
                reconnect(_device); //これももう使っていないです
            }
            return;
        }
        if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            _device.terminate();
        }
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            stopFixScan();
            _workDiscover100 = 0;
            try {
                _device._gatt.discoverServices();
                if (_device._strict != null) {
                    while (++ _workDiscover100 < 20) {
                        Log.e(TAG, "discover challenge " + _workDiscover100);
                        new Handler(Looper.getMainLooper()).postDelayed(() -> {
                            BluetoothGatt g = _device._gatt;
                            if (g != null) {
                                g.discoverServices();
                            }
                        }, 600);
                        synchronized (this) {
                            try {
                                wait(700);
                            }catch(InterruptedException ex) {

                            }
                        }
                    }
                }
            }catch(Throwable ex) {
                Log.e(TAG, ex.getMessage(), ex);
            }
        }
    }

以下は蛇足なのですが、reconnectというメソッドもつくりました。

Java

    public synchronized void reconnect(MABDevice device) throws SecurityException {
        if (device.getStrict() != null && device.getBluetoothDevice() != device.getStrict()) {
            Log.e(TAG, "strict reconnect");
            new Handler(Looper.getMainLooper()).post(() -> {
                if (_device._gatt != null) {
                    _device._gatt.disconnect();;
                    _device._gatt = null;
                }
                new Handler(Looper.getMainLooper()).postDelayed(() -> {
                    BluetoothGatt gatt = device.getStrict().connectGatt(_connectContext, false, MABCallback.this);
                    gatt.connect();
                    _device._gatt = gatt;
                }, 500);
            });
            return;
        }
        _fixTargetAddress = device.getAddress();
        if (MidiNet.isDebug) {
            Log.e(TAG, "reconnect " + _fixTargetAddress);
        }

        if (_fixBluetoothManager == null || _fixScanner == null) {
            _fixBluetoothManager = (BluetoothManager) _connectContext.getSystemService(Context.BLUETOOTH_SERVICE);
            _fixbluetoothAdapter = _fixBluetoothManager.getAdapter();
            _fixScanner = _fixbluetoothAdapter.getBluetoothLeScanner();
            _fixFilters = BleMidiDeviceUtils.getBleMidiScanFilters(_connectContext);
        }

        if (_fixLastScan != 0 && _fixLastScan + 7000 > System.currentTimeMillis()) {
            return;
        } else if (_fixLastScan != 0) {
            _fixScanner.stopScan(_fixCallback);
            _device.getOwner().getThread().pushDelay(() -> {
                reconnect(_device);
            }, 100);
            _fixLastScan = 0;
            return;
        }
        _fixLastScan = System.currentTimeMillis();

        ScanSettings scanSettings = new ScanSettings.Builder()
                .setLegacy(false)
                .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
                .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
                .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                .build();

        _fixScanner.flushPendingScanResults(_fixCallback);
        _fixScanner.startScan(_fixFilters, scanSettings, _fixCallback);
    }
    
    final ScanCallback _fixCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice seek = result.getDevice();
            String addr1 = seek.getAddress();
            if (MidiNet.isDebug) {
                Log.e(TAG, "onScanResult " + addr1 + " check " + _fixTargetAddress);
            }
            if (addr1 != null) {
                if (addr1.equals(_fixTargetAddress)) {
                    long cur = System.currentTimeMillis();
                    /*
                    if (cur < _fixLastConnect + 100) {
                        Log.e(TAG, "skip fix Reconnect " + _fixScanner  + " seeking " + addr1);
                        return;
                    }*/
                    //Log.e(TAG, "trying fix Reconnect " + _fixScanner  + " seeking " + addr1);
                    _fixLastConnect = cur;
                    if (_device._gatt != null) {
                        rererefreshDeviceCache(_device._gatt);
                    }
                    new Handler(Looper.getMainLooper()).post(() -> {
                        BluetoothGatt gatt = seek.connectGatt(_connectContext, false, MABCallback.this);
                        gatt.connect();
                    });
                }
            }
        }
    };

たしかにこれだと、再接続をトライして、何度目かに、ペアリングのダイアログが端末に表示されるのですが、すでにキャッシュされたペアリングと被る場合、ほぼほぼ接続できないようです。なので、今は使ってないです。

こちらのホームページでは、製品MIXandCCへの、GooglePlayダウンロードリンクもあります。ぜひ試してみてください。

広告を表示しています。
タイトルとURLをコピーしました