サンプルコードは、すべてGNU GPL2を想定しています。
最初の対処法としては、201エラーがでたとき、数回リトライするというものでした。(動きません。
@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 順番に処理するクラス
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 順番まち行列クラス
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と用いて、シーケンスのように、レスポンスを待機してから次のタスクを実行します。
@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);
});
}
}
中略
@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タスクが順次実行されるようになります。
データの処理も同様に、単一スレッドからおこないます。
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バイトを想定したほうがうまくいきます。
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に変化したという通知がきて、まったくつながらなくなります。その場合、ペアリングを解除して、再度接続する必要があります。
ペアリングしなおしても、いいのですが、親と子を逆転させるなど、ペアリングを回避できる手段があれば、そのほうがいいです。
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というメソッドもつくりました。
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ダウンロードリンクもあります。ぜひ試してみてください。
広告を表示しています。