Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,29 @@ Connection failures are surfaced as typed `MidiConnectionException` subclasses w

Pass `awaitConnectionTimeout: null` only if you explicitly want to let the required readiness flow wait indefinitely. The optional Apple CoreMIDI handoff remains bounded.

### Filtering discovered BLE devices

BLE scanning is already constrained to the BLE MIDI service, so non-MIDI
peripherals never reach `devices`. To narrow discovery further — for example to
one vendor's hardware — use the UUIDs the peripheral advertised alongside the
MIDI service:

```dart
const vendorService = '0000fe59-0000-1000-8000-00805f9b34fb';

final devices = await midi.devices ?? const <MidiDevice>[];
final vendorDevices =
devices.where((d) => d.serviceUUIDs.contains(vendorService));
```

`MidiDevice.serviceUUIDs` holds lowercase 128-bit UUID strings. It is populated
only for devices discovered over the Dart BLE transport, and is empty for
host-native devices — including Bluetooth devices routed through host MIDI APIs
such as CoreMIDI (see "Host-paired Bluetooth MIDI devices may be native-routed").
Some peripherals split their advertisement and scan response, so a device may be
reported before its full UUID list is known; treat the value as best-effort and
re-read it on `MidiSetupChange` events.

### Setup change events

Listen to `onMidiSetupChanged` to refresh your device list when the host MIDI topology changes. Native desktop/mobile transports monitor platform setup notifications and emit `MidiSetupChange` values:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ bool _isPairingInfoRemoved(Object error) =>
error is UniversalBleException &&
error.message.toLowerCase().contains('pairing information');

/// Advertised service UUIDs as lowercase 128-bit strings, dropping any entry
/// that is not a valid UUID. The pigeon-backed platforms already normalize, but
/// the Linux and Web implementations pass advertisement UUIDs through untouched,
/// so do it here to keep [MidiDevice.serviceUUIDs] comparable everywhere.
List<String> _normalizeServiceUuids(List<String> uuids) => uuids
.map(BleUuidParser.stringOrNull)
.whereType<String>()
.toList(growable: false);

enum _BleHandlerState {
header,
timestamp,
Expand Down Expand Up @@ -67,9 +76,16 @@ class UniversalBleMidiTransport implements MidiBleTransport {
if (result.name == null) {
return;
}
final advertisedServices = _normalizeServiceUuids(result.services);
final existing = _devices[result.deviceId];
if (existing != null) {
existing.name = result.name!;
// Advertisement and scan-response packets arrive separately and only one
// of them carries the service list, so an empty list means "not in this
// packet" rather than "no longer advertised". Keep what we already saw.
if (advertisedServices.isNotEmpty) {
existing.serviceUUIDs = advertisedServices;
}
if (!existing.visible) {
existing.visible = true;
_setupStreamController.add(MidiSetupChange.deviceAppeared);
Expand All @@ -80,7 +96,7 @@ class UniversalBleMidiTransport implements MidiBleTransport {
deviceId: result.deviceId,
name: result.name!,
rxStream: _rxStreamController,
);
)..serviceUUIDs = advertisedServices;
_setupStreamController.add(MidiSetupChange.deviceAppeared);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,85 @@ void main() {
expect(device.connected, isFalse);
});

group('advertised service UUIDs', () {
const vendorService = '0000fe59-0000-1000-8000-00805f9b34fb';

test('are exposed on the discovered device', () async {
fakePlatform.emitScanDevice(
BleDevice(
deviceId: 'ble-uuid',
name: 'UUID Device',
services: <String>[midiServiceId, vendorService],
),
);

final device = (await transport.devices).single;

expect(device.serviceUUIDs, <String>[
midiServiceId.toLowerCase(),
vendorService,
]);
});

test('are normalized to lowercase 128-bit form', () async {
fakePlatform.emitScanDevice(
BleDevice(
deviceId: 'ble-short-uuid',
name: 'Short UUID Device',
services: <String>['180D', 'not-a-uuid'],
),
);

final device = (await transport.devices).single;

expect(device.serviceUUIDs, <String>[
'0000180d-0000-1000-8000-00805f9b34fb',
]);
});

test('survive an advertisement that carries no service list', () async {
fakePlatform.emitScanDevice(
BleDevice(
deviceId: 'ble-uuid',
name: 'UUID Device',
services: <String>[vendorService],
),
);
fakePlatform.emitScanDevice(
BleDevice(
deviceId: 'ble-uuid',
name: 'UUID Device',
services: <String>[],
),
);

final device = (await transport.devices).single;

expect(device.serviceUUIDs, <String>[vendorService]);
});

test('are replaced when a later advertisement lists others', () async {
fakePlatform.emitScanDevice(
BleDevice(
deviceId: 'ble-uuid',
name: 'UUID Device',
services: <String>[vendorService],
),
);
fakePlatform.emitScanDevice(
BleDevice(
deviceId: 'ble-uuid',
name: 'UUID Device',
services: <String>[midiServiceId],
),
);

final device = (await transport.devices).single;

expect(device.serviceUUIDs, <String>[midiServiceId.toLowerCase()]);
});
});

test('teardown unregisters callbacks and can be reactivated', () async {
expect(fakePlatform.onScanResultUpdate, isNotNull);
expect(fakePlatform.onConnectionChange, isNotNull);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ class MidiDevice {

/// Ports that send MIDI data out of this device.
List<MidiPort> outputPorts = [];

/// Service UUIDs advertised by this device, as lowercase 128-bit strings.
///
/// Populated for devices discovered over the Dart BLE transport, where it can
/// be used to narrow discovery to specific hardware. Empty for host-native
/// devices, including Bluetooth devices routed through host MIDI APIs such as
/// CoreMIDI.
List<String> serviceUUIDs = const <String>[];
final StreamController<MidiConnectionState> _connectionStateController =
StreamController<MidiConnectionState>.broadcast();
MidiConnectionState _connectionState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ void main() {
expect(devices.first.outputPorts.first.id, 1);
expect(devices.first.outputPorts.first.type, MidiPortType.OUT);
expect(devices.first.outputPorts.first.connected, isFalse);
// Service UUIDs come from the Dart BLE transport only; the host device
// channel carries none.
expect(devices.first.serviceUUIDs, isEmpty);
});

test('devices prunes stale cached devices when host list changes', () async {
Expand Down