Android、Bluetooth、notifyCharacteristicChangedで、201エラーがでた場合【七転び八起き】

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

最初の対処法としては、201エラーがでたとき、数回リトライするというものでした。(動きません。
当ページのソースコードはJavaの部分を紹介しています。

BleCentralCallback.transferData.java

        @Override
        public boolean transferData(@NonNull byte[] writeBuffer) throws SecurityException {
            try {
                BlePartnerInfo info = _device.getPartnerInfo();
                if (info._gatt == null) {
                    return false;
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    int result = info._gatt.writeCharacteristic(_outputCharacteristic, writeBuffer, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
                    //ここに while(Result == 201) としてループさせていた
    /*
            if (result == 201) {
                int retry = 0;
                do {
                    try {
                        Thread.sleep(50);
                    }catch (InterruptedException ex) {
                    }
                    if ((retry % 10) == 0) {
                        if (MidiOne.isDebug) {
                            Log.e(TAG, "201 retry " + retry);
                        }
                    }
                    ++ retry;
                    if (retry >= 100) {
                        break;
                    }
                    result = info._gatt.writeCharacteristic(_outputCharacteristic, writeBuffer, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
                    if (result != 0 && result != 201) {
                        if (MidiOne.isDebug) {
                            Log.e(TAG, "*** " + result + " retry " + retry);
                        }
                    }
                }while(result == 201);
     */
                    if (MidiOne.isDebug) {
                        //Log.e(TAG, "notify3 " + result + " " + MXUtil.dumpHex(writeBuffer));
                    }
                    return result == BluetoothStatusCodes.SUCCESS;
                } else {
                    _outputCharacteristic.setValue(writeBuffer);
                    boolean result = info._gatt.writeCharacteristic(_outputCharacteristic);
                    if (MidiOne.isDebug) {
                        //Log.e(TAG, "notify4 " + result + " " + MXUtil.dumpHex(writeBuffer));
                    }

                    return result;
                }
            } catch (Throwable ex) {
                _device.getEventCounter().countIt(OneEventCounter.EVENT_ERR_TRANSFER);
                Log.e(TAG, ex.getMessage(), ex);
                // android.os.DeadObjectException will be thrown
                // ignore it
                _device.terminate();
                return false;
            }
        }
    }
    

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

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

OneBleThread.java
package org.star_advance.mixandcc.midinet.bluetooth;

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

import java.util.LinkedList;

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

public class OneBleThread {
    static String TAG = "BluetoothThread";
    public OneBleThread() {
        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();
        }
    }
    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);
            launchThread();
        }else {
            synchronized(this) {
                run.run();
            }
        }
    }

    public void pushIfNotLast(Runnable run) {
        if (_queue.pushIfNotLast(run)) {
            launchThread();
        }
    }

    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();
                }catch (Throwable ex) {
                    Log.e(TAG, ex.getMessage(), ex);
                }
                if (true) {
                    long stepTime = System.currentTimeMillis();
                    long time1 = 10- (stepTime - prevtime);
                    if (time1 < 2) {
                        time1 = 2;
                    }
                    else if (time1 >= 10) {
                        time1 = 10;
                    }
                    MidiOne.Thread_sleep(time1);
                    prevtime = stepTime;
                }
            }
        }finally {
            _thread = null;
        }
    }

}

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

BleCentralCallback.java
package org.star_advance.mixandcc.midinet.bluetooth;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;

import org.star_advance.mixandcc.midinet.MidiOne;
import org.star_advance.mixandcc.midinet.util.BleMidiDeviceUtils;
import org.star_advance.mixandcc.midinet.util.BleUuidUtils;
import org.star_advance.mixandcc.libs.MXUtil;
import org.star_advance.mixandcc.midinet.v1.OneEventCounter;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

public final class BleCentralCallback extends BluetoothGattCallback {

    static final String TAG = "BleCentralCallback";
    OneDeviceBle _device;
    String _address;
    public BleCentralCallback(Context applicationContext, OneBleCore mab, OneDeviceBle device) {
        super();
        _device = device;
        _connectContext = applicationContext;
    }

    long _fixLastScan = 0;
    OneDeviceBle _fixTargetAddress = null;

    long _fixLastConnect = 0;

    Context _connectContext;

    final ScanCallback _fixCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice seek = result.getDevice();
            String addr1 = seek.getAddress();
            if (addr1 != null) {
                if (addr1.equals(_fixTargetAddress.getPartnerInfo()._bleDevice.getAddress())) {
                    if (MidiOne.isDebug) {
                        //Log.e(TAG, "fix scanned " + addr1 + " == " + _fixTargetAddress);
                    }
                    long cur = System.currentTimeMillis();
                    _fixLastConnect = cur;
                    new Handler(Looper.getMainLooper()).post(() -> {
                        _workDiscover100 = 0;
                        _fixbluetoothAdapter.cancelDiscovery(); //ADDED 2025/4/25
                        _fixTargetAddress.getPartnerInfo()._connectedGatt = seek.connectGatt(_connectContext, false, BleCentralCallback.this);
                        _fixTargetAddress.getPartnerInfo()._connectedGatt.connect();
                    });
                }
                else {
                    if (MidiOne.isDebug) {
                        //Log.e(TAG, "fix scanned " + addr1 + " <> " + _fixTargetAddress);
                    }
                }
            }
        }
    };

    BluetoothManager _fixBluetoothManager = null;
    BluetoothAdapter _fixbluetoothAdapter = null;
    BluetoothLeScanner _fixScanner = null;
    List<ScanFilter> _fixFilters = null;

    public void stopFixScan() {
        if (_fixScanner != null) {
            if (MidiOne.isDebug) {
                Log.e(TAG, "stopFixScan");
            }
            _fixScanner.stopScan(_fixCallback);
        }
    }

    public synchronized void reconnect(OneDeviceBle device) throws SecurityException {
        try {
            _fixTargetAddress = device;
        }catch (Throwable ex) {
            Log.e(TAG, "can't reconnect " + device);
            return;
        }
        if (MidiOne.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.getPartnerInfo()._oneBle.getThread().pushDelay(() -> {
                reconnect(_device);
            }, 500);
            _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);
    }

    int _workDiscover100 = -1;
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) throws SecurityException {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onConnectionStateChange " + status + " -> " + newState + " / " + gatt);
        }
        /*
        if (status == 0 && status == 0) {
            BluetoothDevice device = _device.getPartnerInfo()._bleDevice;
            if (device != null) {
                if (true) {
                }
                else {
                    String seekName = device.getName();
                    String seekAddr = device.getAddress();
                    Set<BluetoothDevice> listAlready = _device.getPartnerInfo()._oneBle.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 != device && name.equals(seekName)) {
                                err++;
                            }
                        }
                        if (err >= 1) {
                            _device.getEventCounter().countIt(OneEventCounter.EVENT_ERR_CONNECT);
                            reconnect(_device);
                        }
                    }
                }
            }
        }
         */
        /*
        if (_device.getPartnerInfo()._gatt == null) {
            _device.getPartnerInfo()._gatt = gatt;
        }*/
        if (gatt == null) {
            Log.e(TAG, "gatt = null");
            return;
            //gatt = _device.getPartnerInfo()._gatt;
        }
        /*
        if (_device.getPartnerInfo()._gatt == gatt) {
            if (MidiOne.isDebug) {
                Log.e(TAG, "gatt = 0");
            }
        }else {
            if (_device.getPartnerInfo()._gatt.getDevice() == gatt.getDevice()) {
                if (MidiOne.isDebug) {
                    Log.e(TAG, "gatt = 1");
                }
                //gatt = _device.getPartnerInfo()._gatt ;
                _device.getPartnerInfo()._gatt = gatt;
            }
            else if (_device.getPartnerInfo()._gatt.getDevice().getAddress().equals(gatt.getDevice().getAddress()) == true) {
                if (MidiOne.isDebug) {
                    Log.e(TAG, "gatt = 2");
                }
                //gatt = _device.getPartnerInfo()._gatt ;
                _device.getPartnerInfo()._gatt = gatt;
            }
            else {
                if (MidiOne.isDebug) {
                    Log.e(TAG, "gatt = 3");
                }
                _device.getPartnerInfo()._gatt = gatt;
            }
        }*/
        if (newState == BluetoothProfile.STATE_DISCONNECTED && (status == 133 || status == 0)) {
            _device.getEventCounter().countIt(OneEventCounter.EVENT_ERR_CONNECT);
            reconnect(_device);
            return;
        }
        if (newState == BluetoothProfile.STATE_DISCONNECTED && status == 8) {
            //jut time out
            _device.getEventCounter().countIt(OneEventCounter.EVENT_ERR_CONNECT);
            reconnect(_device);
            return;
        }
        if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            _device.getEventCounter().countIt(OneEventCounter.EVENT_DISCONNECTED);
            MidiOne.getInstance().getDeviceEnumerator().fireOnDeviceConnectionChanged(_device);
        }
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            startDiscovery(gatt);
        }
    }

    public void startDiscovery(BluetoothGatt gatt) {
        if (_workDiscover100 == 0) {
            _workDiscover100 ++;
            Log.e(TAG, "discover challenge " + _workDiscover100);
            if (MidiOne.getInstance().getBLEAdapter().isDiscovering()) { //ADDED 2025/4/25
                MidiOne.getInstance().getBLEAdapter().cancelDiscovery();
            }
            gatt.discoverServices();
            try {
                _device.getPartnerInfo()._oneBle.getThread().pushDelay(() -> {
                    while (++ _workDiscover100 < 100) {
                        Log.e(TAG, "discover challenge " + _workDiscover100);
                        new Handler(Looper.getMainLooper()).postDelayed(() -> {
                            gatt.discoverServices();
                        }, 10);
                        synchronized (this) {
                            try {
                                wait(20);
                            }catch(InterruptedException ex) {

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

    @Override
    public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
        BlePartnerInfo info = _device.getPartnerInfo();
        OneBleThread thread = info._oneBle.getThread();

        if (MidiOne.isDebug) {
            Log.e(TAG, "onServicesDiscovered " + status);
        }
        _workDiscover100 = 200;
        /*
        if (gatt != null) {
            info._gatt = gatt;
        }*/
        if (status != BluetoothGatt.GATT_SUCCESS) {
            return;
        }
        stopFixScan();

        BluetoothGattService deviceInformationService = BleMidiDeviceUtils.getDeviceInformationService(gatt);
        if (deviceInformationService != null) {

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

            final BluetoothGattCharacteristic modelCharacteristic = BleMidiDeviceUtils.getModelCharacteristic(deviceInformationService);
            if (modelCharacteristic != null) {
                if (MidiOne.isDebug) {
                    Log.e(TAG, "read modelCharacteristic");
                }
                thread.push(() -> {
                    // this calls onCharacteristicRead after completed
                    thread.pauseTillResponse();
                    gatt.readCharacteristic(modelCharacteristic);
                });
            }
        }
        // if the app is running on Meta/Oculus, don't set the mtu
        try {
            Log.e(TAG, "device = " + Build.DEVICE);
            boolean isOculusDevices = false;
            isOculusDevices |= "miramar".equals(Build.DEVICE);
            isOculusDevices |= "hollywood".equals(Build.DEVICE);
            isOculusDevices |= "eureka".equals(Build.DEVICE);
            //isOculusDevices |= "raspite".equals(Build.DEVICE);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE || isOculusDevices) {
                // Android 14: the default MTU size set to 517
                // https://developer.android.com/about/versions/14/behavior-changes-all#mtu-set-to-517
                _device.setBufferSize(64);
            } else {
                thread.push(() -> {
                    // this calls onCharacteristicRead after completed
                    thread.pauseTillResponse();
                    gatt.requestMtu(64);
                });
            }
        }catch(Throwable ex) {
            Log.e(TAG, ex.getMessage(), ex);
        }
        if (info._internalIn != null) {
            info._internalIn.stopParserAndThread();
        }
        thread.push(() -> {
            try {
                if (info._internalIn == null) {
                    info._internalIn = new InternalMidiInputDevice(gatt);
                    ((InternalMidiInputDevice)info._internalIn).configureBleProtocol(gatt);
                }
                info._internalIn.bindOnParsed(_device.getInput(0));
                info._internalIn.startParser();
            } catch (IllegalArgumentException ex) {
                new Handler(Looper.getMainLooper()).post(() -> {
                    Toast.makeText(_connectContext, "Can't use It as MIDI (" + ex.getMessage() + ")", Toast.LENGTH_SHORT).show();
                });
            }
        });

        if (info._internalOut != null) {
            info._internalOut.stopTransmitterAndThread();
            info._internalOut = null;
        }
        thread.push(() -> {
            try {
                if (info._internalOut == null) {
                    info._internalOut = new InternalMidiOutputDevice(gatt, _device.getPartnerInfo());
                    ((InternalMidiOutputDevice)info._internalOut).configureBleProtocol();
                }
                info._internalOut.startTransmitter();
            } catch (IllegalArgumentException ex) {
                new Handler(Looper.getMainLooper()).post(() -> {
                    Toast.makeText(_connectContext, "Can't use It as MIDI (" + ex.getMessage() + ")", Toast.LENGTH_SHORT).show();
                });
            }
        });
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            thread.push(() -> {
                // Set the connection priority to high(for low latency)
                gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
            });
        }
        thread.push(() -> {
            _device.getEventCounter().countIt(OneEventCounter.EVENT_CONNECTED);
            MidiOne.getInstance().getDeviceEnumerator().fireOnInputOpened(_device.getInput(0));
            MidiOne.getInstance().getDeviceEnumerator().fireOnOutputOpened(_device.getOutput(0));
            MidiOne.getInstance().getDeviceEnumerator().fireOnDeviceConnectionChanged(_device);
        });
    }

    @Override
    public void onCharacteristicChanged(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
        BlePartnerInfo info = _device.getPartnerInfo();
        if (MidiOne.isDebug) {
            //Log.e(TAG, "onCharacteristicChanged 0, " + MXUtil.dumpHex(value));
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (info._internalIn != null) {
                info._internalIn.incomingData(value);
            } else {
                if (MidiOne.isDebug) {
                    Log.e(TAG, "skipped)");
                }
            }
        } else {
            if (info._internalIn != null) {
                info._internalIn.incomingData(characteristic.getValue());
            } else {
                if (MidiOne.isDebug) {
                    Log.e(TAG, "skipped)");
                }
            }
        }
    }

    @Override
    @Deprecated
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        if (MidiOne.isDebug) {
            //Log.e(TAG, "onCharacteristicChanged 1,");
        }
        /*
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        } else*/ {
            BlePartnerInfo info = _device.getPartnerInfo();
            if (info._internalIn != null) {
                info._internalIn.incomingData(characteristic.getValue());
            }
        }
    }

    @Override
    @Deprecated
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
                                     int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onCharacteristicRead d " + MXUtil.dumpHex(characteristic.getValue()));
        }
        byte[] value = characteristic.getValue();
        BlePartnerInfo info = _device.getPartnerInfo();
        try {
            if (BleUuidUtils.matches(characteristic.getUuid(), BleMidiDeviceUtils.CHARACTERISTIC_MANUFACTURER_NAME) && value != null && value.length > 0) {
                String manufacturer = new String(value);

                info._manufacture = manufacturer;
                if (MidiOne.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);

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

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

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

                info._manufacture = manufacturer;
                if (MidiOne.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);

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

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

    @Override
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onMtuChanged " + mtu);
        }
        if (_device != null) {
            _device.setBufferSize(mtu < 23 ? 20 : mtu - 3);
        }
        BlePartnerInfo info = _device.getPartnerInfo();
        info._oneBle.getThread().caughtResponse();
    }

    /**
     * Terminates callback
     */
    public void terminate() throws SecurityException {
        BlePartnerInfo info = _device.getPartnerInfo();
        if (info._internalIn != null) {
            info._internalIn.stopParserAndThread();
        }
        if (info._internalOut != null) {
            info._internalOut.stopTransmitterAndThread();
        }
        if (info._connectedGatt != null) {
            info._connectedGatt.disconnect();
            info._connectedGatt = null;
        }
        _workDiscover100 = -1;
    }
    public class InternalMidiInputDevice extends BleInputBase {
        public InternalMidiInputDevice(BluetoothGatt gatt) throws IllegalArgumentException, SecurityException {
            super(_device.getPartnerInfo());
            BlePartnerInfo info = _device.getPartnerInfo();

            BluetoothGattService midiService = BleMidiDeviceUtils.getMidiService(_connectContext, gatt);
            if (midiService == null) {
                List<UUID> uuidList = new ArrayList<>();
                for (BluetoothGattService service : gatt.getServices()) {
                    uuidList.add(service.getUuid());
                }
                throw new IllegalArgumentException("MIDI GattService not found from '" + gatt.getDevice().getName() + "'. Service UUIDs:" + Arrays.toString(uuidList.toArray()));
            }
        }

        BluetoothGattCharacteristic _inputCharacteristic;

        public void configureBleProtocol(BluetoothGatt gatt) throws SecurityException {
            BlePartnerInfo info = _device.getPartnerInfo();
            info._oneBle.getThread().push(() -> {

                BluetoothGattService midiService = BleMidiDeviceUtils.getMidiService(_connectContext, gatt);
                if (midiService == null) {
                    List<UUID> uuidList = new ArrayList<>();
                    for (BluetoothGattService service : gatt.getServices()) {
                        uuidList.add(service.getUuid());
                    }
                    throw new IllegalArgumentException("MIDI GattService not found from '" +info._bleDevice.getAddress() + "'. Service UUIDs:" + Arrays.toString(uuidList.toArray()));
                }

                _inputCharacteristic = BleMidiDeviceUtils.getMidiInputCharacteristic(_connectContext, midiService);
                if (_inputCharacteristic == null) {
                    throw new IllegalArgumentException("MIDI Input GattCharacteristic not found. Service UUID:" + midiService.getUuid());
                }

                gatt.setCharacteristicNotification(_inputCharacteristic, true);
                List<BluetoothGattDescriptor> descriptors = _inputCharacteristic.getDescriptors();
                for (BluetoothGattDescriptor descriptor : descriptors) {
                    if (BleUuidUtils.matches(BleUuidUtils.fromShortValue(0x2902), descriptor.getUuid())) {
                        info._oneBle.getThread().push(() -> {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                                gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                            } else {
                                descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
                                gatt.writeDescriptor(descriptor);
                            }
                            info._oneBle.getThread().pauseTillResponse();
                        });
                    }
                }
                info._oneBle.getThread().push(() -> {
                    info._oneBle.getThread().pauseTillResponse();
                    gatt.readCharacteristic(_inputCharacteristic);
                });
            });
        }


        @NonNull
        public String getAddress() {
            return _device.getPartnerInfo()._bleDevice.getAddress();
        }
    }

    public final class InternalMidiOutputDevice extends BleOutputBase {
        private int bufferSize = 30;

        BluetoothGattCharacteristic _outputCharacteristic;

        public InternalMidiOutputDevice(BluetoothGatt gatt, BlePartnerInfo target) throws IllegalArgumentException, SecurityException {
            super(target._oneDevice, target);
            BlePartnerInfo info = _device.getPartnerInfo();

            BluetoothGattService midiService = BleMidiDeviceUtils.getMidiService(_connectContext, gatt);
            if (midiService == null) {
                List<UUID> uuidList = new ArrayList<>();
                for (BluetoothGattService service : gatt.getServices()) {
                    uuidList.add(service.getUuid());
                }
                throw new IllegalArgumentException("MIDI GattService not found from '" + gatt.getDevice().getName() + "'. Service UUIDs:" + Arrays.toString(uuidList.toArray()));
            }

            _outputCharacteristic = BleMidiDeviceUtils.getMidiOutputCharacteristic(_connectContext, midiService);
            if (_outputCharacteristic == null) {
                throw new IllegalArgumentException("MIDI Output GattCharacteristic not found. Service UUID:" + midiService.getUuid());
            }
        }

        /**
         * Configure the device as BLE Central
         */
        public void configureBleProtocol() {
            _outputCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
        }

        @Override
        public boolean transferData(@NonNull byte[] writeBuffer) throws SecurityException {
            try {
                BlePartnerInfo info = _device.getPartnerInfo();
                if (info._connectedGatt == null) {
                    return false;
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    int result = info._connectedGatt.writeCharacteristic(_outputCharacteristic, writeBuffer, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
                    if (MidiOne.isDebug) {
                        //Log.e(TAG, "notify3 " + result + " " + MXUtil.dumpHex(writeBuffer));
                    }
                    return result == BluetoothStatusCodes.SUCCESS;
                } else {
                    _outputCharacteristic.setValue(writeBuffer);
                    boolean result = info._connectedGatt.writeCharacteristic(_outputCharacteristic);
                    if (MidiOne.isDebug) {
//                      //Log.e(TAG, "notify4 " + result + " " + MXUtil.dumpHex(writeBuffer));
                    }

                    return result;
                }
            } catch (Throwable ex) {
                _device.getEventCounter().countIt(OneEventCounter.EVENT_ERR_TRANSFER);
                Log.e(TAG, ex.getMessage(), ex);
                // android.os.DeadObjectException will be thrown
                // ignore it
                _device.terminate();
                return false;
            }
        }
    }

    @Override
    public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onDescriptorWrite " + MXUtil.dumpHex(descriptor.getValue()));
        }
        BlePartnerInfo info = _device.getPartnerInfo();
        info._oneBle.getThread().caughtResponse();
    }

    boolean rererefreshDeviceCache(BluetoothGatt gatt) {
        try {
            Method m = gatt.getClass().getMethod("refresh");
            Boolean b = (Boolean) m.invoke(gatt);
            return b.booleanValue();
        }catch(Throwable ex) {
            Log.e(TAG, ex.getMessage(), ex);
        }
        return false;
    }

    public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onPhyUpdate " + gatt + "," + status);
        }
    }

    public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onPhyRead " + gatt + "," + status);
        }
    }
    public void onCharacteristicWrite(
            BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        if (MidiOne.isDebug) {
            //Log.e(TAG, "onCharacteristicWrite " + gatt + "," + status + ", "+  MXUtil.dumpHex(characteristic.getValue()), new Throwable());
        }
        super.onCharacteristicWrite(gatt, characteristic, status);
    }

    @Deprecated
    public void onDescriptorRead(
            BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onDescriptorRead1 " + gatt + "," + status);
        }
        super.onDescriptorRead(gatt, descriptor, status);
    }

    public void onDescriptorRead(
            @NonNull BluetoothGatt gatt,
            @NonNull BluetoothGattDescriptor descriptor,
            int status,
            byte[] value) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onDescriptorRead2 " + gatt + "," + status);
        }
    }
    public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onReliableWriteCompleted " + gatt + "," + status);
        }
    }

    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onReadRemoteRssi " + gatt + ", " + rssi + "," + status);
        }
    }

    public void onConnectionUpdated(
            BluetoothGatt gatt, int interval, int latency, int timeout, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onConnectionUpdated " + status + " interval " + interval);
        }
        if (interval >= 1 && interval <= 50) {
            _device.getPartnerInfo().CONNECTION_INTERVAL = interval;
        }
    }

    public void onConnectionUpdated(BluetoothDevice device, int interval, int latency, int timeout, int status) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onConnectionUpdated " + status + " interval " + interval);
        }
        if (interval >= 1 && interval <= 50) {
            _device.getPartnerInfo().CONNECTION_INTERVAL = interval;
        }
    }
    public void onServiceChanged(BluetoothGatt gatt) {
        if (MidiOne.isDebug) {
            Log.e(TAG, "onServiceChanged " + gatt);
        }
    }

    public void onSubrateChange(
            BluetoothGatt gatt,
            int subrateFactor,
            int latency,
            int contNu,
            int timeout,
            int status) {

        if (MidiOne.isDebug) {
            Log.e(TAG, "onSubrateChange ");
        }
    }

    /*
                    if (result == 201) {
                        int retry = 0;
                        do {
                            try {
                                Thread.sleep(50);
                            }catch (InterruptedException ex) {
                            }
                            if ((retry % 10) == 0) {
                                if (MidiOne.isDebug) {
                                    Log.e(TAG, "201 retry " + retry);
                                }
                            }
                            ++ retry;
                            if (retry >= 100) {
                                break;
                            }
                            result = info._gatt.writeCharacteristic(_outputCharacteristic, writeBuffer, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
                            if (result != 0 && result != 201) {
                                if (MidiOne.isDebug) {
                                    Log.e(TAG, "*** " + result + " retry " + retry);
                                }
                            }
                        }while(result == 201);
     */
}

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

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

BleOutputBase.java
package org.star_advance.mixandcc.midinet.bluetooth;

import android.util.Log;

import androidx.annotation.NonNull;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import org.star_advance.mixandcc.libs.MXMidiStatic;
import org.star_advance.mixandcc.libs.MXQueue;
import org.star_advance.mixandcc.libs.MXUtil;
import org.star_advance.mixandcc.midinet.MidiOne;
import org.star_advance.mixandcc.midinet.v1.OneAbstractByteBuilder;
import org.star_advance.mixandcc.midinet.v1.OneDevice;
import org.star_advance.mixandcc.midinet.v1.OneEventCounter;
import org.star_advance.mixandcc.midinet.v1.OneMessage;
import org.star_advance.mixandcc.midinet.v1.OneOutput;


public abstract class BleOutputBase extends OneOutput {

    static final String TAG = "MidiOutputDevice";
    MXQueue<OneMessage> _queue = new MXQueue<>();
    boolean _transmitterStarted = false;
    BlePartnerInfo _info;

    public BleOutputBase(OneDevice device, BlePartnerInfo info) {
        super(device);
        _info = info;
    }

    /*
     * Transfer data
     *
     * @param writeBuffer byte array to write
     * @return true if transfer succeed
     */
    protected abstract boolean transferData(@NonNull byte[] writeBuffer);

    public synchronized boolean transferStream(OneAbstractByteBuilder stream) {
        long start = System.currentTimeMillis();
        byte[] data2 = stream.toCachedBuffer();
        if (data2.length == 0) {
            return true;
        }
        while (!transferData(data2)) {
            OneDevice d = _info._oneDevice;
            if(d == null) {
                return false;
            }
            d.getEventCounter().countIt(OneEventCounter.EVENT_ERR_TRANSFER);
            if (MidiOne.isDebug || true) {
                Log.e(TAG, "err transfer " + d.getName() + " :" + MXUtil.dumpHex(data2));
            }
            MidiOne.Thread_sleep(_info.CONNECTION_INTERVAL);
            if (System.currentTimeMillis() >= start + 1000) {
                Log.e(TAG, "no more");
                return false;
            }
        }
        MidiOne.Thread_sleep(_info.CONNECTION_INTERVAL);
        return true;
    }

    /**
     * Obtains buffer size
     *
     * @return buffer size
     */
    int _transferBufferSize = 517;

    public int getBufferSize() {
        return _transferBufferSize;
    }

    public void setBufferSize(int bufferSize) {
        _transferBufferSize = bufferSize;
    }

    @NonNull

    OneAbstractByteBuilder _stream = null;
    public void dequeAndSend() {
        int packetMax = getBufferSize();
        if (packetMax > _info.PACKET_MAX) {
            packetMax = _info.PACKET_MAX;
        }
        if (_stream == null || _stream.getRawData().length > packetMax) {
            _stream = new OneAbstractByteBuilder(packetMax) {
                @Override
                public boolean append(OneMessage one) {
                    try {
                        if (size() == 0) {
                            if (size() + 2 + one._data.length >= _rawData.length) {
                                return false;
                            }
                            write((byte) (0x80 | ((one._tick >> 7) & 0x3f)));
                            write((byte) (0x80 | (one._tick & 0x7f)));
                            write(one._data, 0, one._data.length);
                            return true;
                        }
                        else {
                            if (size() + 1 + one._data.length >= _rawData.length) {
                                return false;
                            }
                            write((byte) (0x80 | (one._tick & 0x7f)));
                            write(one._data, 0, one._data.length);
                            return true;
                        }
                    } catch (IOException ex) {
                        Log.e(TAG, ex.getMessage(), ex);
                        return false;
                    }
                }
            };
            _stream.setAutoExtendable(false);//for META message
        }
        _stream.reset();
        while (_queue.isEmpty() == false) {
            OneMessage packet0 = _queue.pop();
            if (packet0 == null) {
                continue;
            }
            if (packet0._data == null || packet0._data.length == 0) {
                continue;
            }

            int isLong = packet0._data[0] & 0xff;

            if (isLong == 0xf0 || isLong == 0xf7) {
                sendMidiSystemExclusive(packet0);
                _stream.reset();
                MidiOne.Thread_sleep(_info.CONNECTION_INTERVAL);
            }

            if (_stream.append(packet0) == false) {
                if (packet0._data.length + 2 < packetMax) {
                    if (MidiOne.isDebug) {
                        Log.d(TAG, "packet back " + packetMax + " > " + packet0._data.length + "+2");
                    }
                    _queue.back(packet0);
                    continue;
                }
                else {
                    if (MidiOne.isDebug) {
                        Log.i(TAG, "packet too large " + packet0 + " size " + packet0._data.length + "+2 (limit " + packetMax);
                    }
                    continue;
                }
            }
            while (!_queue.isEmpty()) {
                OneMessage packet = _queue.pop();
                boolean sysex2 = (packet._data[0] & 0xff) == 0xf0;
                if (sysex2) {
                    _queue.back(packet);
                    break;
                }
                if (!_stream.append(packet)) {
                    _queue.back(packet);
                    break;
                }
            }

            if (_stream.size() > 0) {
                transferStream(_stream);
                MidiOne.Thread_sleep(_info.CONNECTION_INTERVAL);
                _stream.reset();
            }
        }
    }

    /**
     * Starts using the device
     */
    public synchronized void startTransmitter() {
        _transmitterStarted = true;
        notifyAll();
    }

    /**
     * Stops using the device
     */
    public void stopTransmitterAndThread() {
        _transmitterStarted = false;
    }

    /**
     * Terminates the device instance
     */
    public final void terminate() {
        _queue.quit();
    }

    public boolean isRunning() {
        return _transmitterStarted;
    }

    @Override
    public boolean dispatchOne(OneMessage one) {
        if (one._data.length > 0) {
            startTransmitter();
            one._tick = getTimestamp();
            _queue.push(one);
            _info._oneBle.getThread().pushIfNotLast(this::dequeAndSend);
        }
        return true;
    }

    long pastTimestamp = -1;

    public int getTimestamp1st(long timestamp) {
        int forskip1 = (int) (0x80 | (timestamp & 0x7f)) & 0xff;
        return forskip1;
    }

    public int getTimestamp2nd(long timestamp) {
        int forskip2 = (int) (0x80 | ((timestamp >> 7) & 0x3f)) & 0xff;
        return forskip2;
    }

    protected long getTimestamp() {
        if (true) {
            return 0;
        }
        long milliseconds = System.currentTimeMillis() % 8192;
        if (pastTimestamp >= milliseconds) {
            milliseconds = pastTimestamp + 1;
        }
        pastTimestamp = milliseconds;

        int max = 0x80 * 0x40;
        int skip = 0x40 + 1;
        long timestamp = milliseconds & (max - skip);

        int forskip1 = getTimestamp1st(timestamp);
        int forskip2 = getTimestamp2nd(timestamp);

        if (forskip1 >= 0xf0) {
            pastTimestamp++;
            return getTimestamp();
        }
        if (forskip2 >= 0xf0) {
            pastTimestamp += 1 << 7;
            return getTimestamp();
        }
        return timestamp;
    }

    protected boolean sendMidiSystemExclusive(OneMessage data) {
        int bufferSize = getBufferSize();
        if (bufferSize >= _info.PACKET_MAX_SYSEX) {
            bufferSize = _info.PACKET_MAX_SYSEX;
        }
        OneAbstractByteBuilder sysexStream = new OneAbstractByteBuilder(bufferSize) {
            @Override
            public boolean append(OneMessage one) {
                return false;
            }
        };

        long timestamp = data._tick;
        ByteArrayInputStream in = new ByteArrayInputStream(data._data);

        sysexStream.reset();
        do {
            timestamp ++;
        } while (getTimestamp1st(timestamp) == MXMidiStatic.COMMAND_SYSEX_END);
        boolean first = true;


        while(true) {
            if (first) {
                sysexStream.tryWrite(getTimestamp2nd(timestamp));
                sysexStream.tryWrite(getTimestamp1st(timestamp));
                first = false;
            }
            else {
                sysexStream.tryWrite(getTimestamp1st(timestamp));
            }

            int ch = 0;
            while(sysexStream.countSpace() >= 2) {
                ch = in.read();
                if (ch < 0 || ch == 0xf7) {
                    break;
                }
                sysexStream.tryWrite(ch);
            }
            if (in.available() >= 1 && sysexStream.countSpace() >= 1) {
                ch = in.read();
                sysexStream.tryWrite(ch);
            }
            if (sysexStream.size() <= 2) {
                break;
            }
            if (ch == 0xf7) {
                sysexStream.tryWrite(getTimestamp1st(timestamp));
                sysexStream.tryWrite(0xf7);
            }

            if (transferStream(sysexStream) == false) {
                break;
            }
            MidiOne.Thread_sleep(_info.CONNECTION_INTERVAL);
            sysexStream.reset();
        }
        return true;
    }


    @Override
    public int getLabel() {
        return 0;
    }

    @Override
    public String getLabelText() {
        return "";
    }

    @Override
    public void onClose() {

    }
}

このように、転送したいパケットを、バッファーサイズにあわせて送信する関数を用意したあと、取りこぼしを発生させないため、OneBleThreadで、待機します。電波状況によっては、これがあったほうが安定します。onConnectionUpdatedでわたされたIntervalをもとに、お休みを入れます。1~50の場合だけ受け入れるようにしています。こちらの複数の端末では、12~35程度がわたされていました。

なおバッファーサイズは、MTUで指定されたサイズ(517?)より、Bluetoothの互換性ある一般的な23バイトを想定したほうがうまくいきます。SysEXは16バイトじゃないと動かないものもありました。(結果論なのですが、YAMAHAのV50とMD-BT01は、16バイトで区切らないと、SysEXやりとりに失敗しています。)

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

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


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

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

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

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

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

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

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

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

広告を表示しています。

タイトルとURLをコピーしました