サンプルコードは、すべてGNU GPL2を想定しています。
最初の対処法としては、201エラーがでたとき、数回リトライするというものでした。(動きません。
当ページのソースコードは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.javapackage 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.javapackage 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.javapackage 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ダウンロードリンクもあります。ぜひ試してみてください。
広告を表示しています。