Появление MAUI дало разработчикам Net возможность разрабатывать многоплатформенные приложения. MAUI является развитием Xamarin.Forms, но имеет более высокую производительность, более высокую масштабируемость и более простую структуру, чем Xamarin. Однако реализация MAUI, связанная с платформой, является неполной. Поэтому команда MASA запустила экспериментальный проект, призванный дополнить и расширить Microsoft MAUI.
Адрес проекта https://github.com/BlazorComponent/MASA.Blazor/tree/main/src/Masa.Blazor.Maui.Plugin
Для каждой функции есть отдельный демо-проект.,Учитывая размер установочного файла приложения (хотя в MAUI встроена функция обрезки,Но эта функция влияет на сам код),Каждая функция будет предоставлена в виде отдельного пакета nuget.,Удобный,Проект только начался сейчас,Но я верю, что скоро появится контент, который можно будет доставить.
Эта серия статей предназначена для новичков в мобильной разработке, которые разрабатывают функции, связанные с платформой, с нуля и демонстрируют, как использовать технологию MAUI для разработки соответствующих функций, обращаясь к официальной документации платформы.
В предыдущей статье мы реализовали функцию сканирования Bluetooth BLE. Здесь мы продолжаем реализовывать функцию связи. Коды, связанные с JAVA, в этой статье взяты с официального сайта разработчика Android.
Общий профиль атрибутов Общий профиль атрибутов называется GATT. ГАТТ определяет типы атрибутов и оговаривает, как их использовать, включая структуру передачи и хранения данных, а также некоторые основные операции. Он содержит некоторые понятия, такие как характеристики, услуги и т. д. Он также определяет процесс обнаружения служб, функций и связей между службами, включая чтение и запись значений функций. В качестве примера мы используем Quectel FC410.
бын РФ Инструмент подключения может просматривать конфигурацию устройства. В устройстве имеется основная служба с префиксом FFFF. В рамках этой службы имеется функция с префиксом FF01. и пишем два атрибута (если есть Notify, то будет и дескриптор). Другими словами, мы можем отправлять данные на устройство с помощью этой функции и получать обратную информацию от устройства через Bluetooth, подписавшись на событие изменения значения функции. и BLE Первый шаг взаимодействия с устройством — Подключиться. к серверу ГАТТ. Точнее, Подключиться кна устройстве GATT сервер. Давайте сначала посмотрим на реализацию JAVA.
JAVAкод
bluetoothGatt = device.connectGatt(this, false, gattCallback);
Чтобы подключиться к серверу GATT на устройстве BLE, вам необходимо использовать метод ConnectGatt(). Этот метод принимает три параметра: объект Context, autoConnect (логическое значение, указывающее, следует ли автоматически подключаться к устройству BLE, если оно доступно) и ссылку на BluetoothGattCallback. Этот метод извлекает экземпляр BluetoothGatt, который затем можно использовать для выполнения клиентских операций GATT. Вызывающий абонент (приложение Android) является клиентом GATT. BluetoothGattCallback используется для передачи результатов (например, состояния соединения) клиенту, а также любых дальнейших клиентских операций GATT. Давайте еще раз взглянем на JAVA-реализацию BluetoothGattCallback.
JAVA код
// Various callback methods defined by the BLE API.
private final BluetoothGattCallback gattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = ACTION_GATT_CONNECTED;
connectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
Log.i(TAG, "Attempting to start service discovery:" +
bluetoothGatt.discoverServices());
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = ACTION_GATT_DISCONNECTED;
connectionState = STATE_DISCONNECTED;
Log.i(TAG, "Disconnected from GATT server.");
broadcastUpdate(intentAction);
}
}
@Override
// New services discovered
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
@Override
// Result of a characteristic read operation
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
}
...
Потому что в будущем нам необходимо реализовать функции других платформ.,Идея состоит в том, чтобы поместить все общедоступные части в корневой каталог проекта.,Реализация для конкретной платформы,Поместите его в папку соответствующей платформы в соответствующем каталоге «Платформы».,Затем организуйте структуру классов с помощью частичных классов.。Методы, специфичные для платформы, названы в честьPlatformкак префикс。Мы здесь первыеMasa.Blazor.Maui.Plugin.BluetoothпроектPlatforms->AndroidСоздайте новый каталог с именемRemoteGattServer.android.csчастичный класс,Затем добавьте метод инициализации и BluetoothGattCallback.
partial class RemoteGattServer
{
private Android.Bluetooth.BluetoothGatt _gatt;
private Android.Bluetooth.BluetoothGattCallback _gattCallback;
private void PlatformInit()
{
_gattCallback = new GattCallback(this);
_gatt = ((Android.Bluetooth.BluetoothDevice)Device).ConnectGatt(Android.App.Application.Context, false, _gattCallback);
}
public static implicit operator Android.Bluetooth.BluetoothGatt(RemoteGattServer gatt)
{
return gatt._gatt;
}
internal event EventHandler<CharacteristicEventArgs> CharacteristicRead;
internal event EventHandler<GattEventArgs> ServicesDiscovered;
private bool _servicesDiscovered = false;
...
internal class GattCallback : Android.Bluetooth.BluetoothGattCallback
{
private readonly RemoteGattServer _remoteGattServer;
internal GattCallback(RemoteGattServer remoteGattServer)
{
_remoteGattServer = remoteGattServer;
}
...
public override void OnCharacteristicWrite(Android.Bluetooth.BluetoothGatt gatt, Android.Bluetooth.BluetoothGattCharacteristic characteristic, Android.Bluetooth.GattStatus status)
{
System.Diagnostics.Debug.WriteLine($"CharacteristicWrite {characteristic.Uuid} Status:{status}");
_remoteGattServer.CharacteristicWrite?.Invoke(_remoteGattServer, new CharacteristicEventArgs { Characteristic = characteristic, Status = status });
}
}
}
...
internal class ConnectionStateEventArgs : GattEventArgs
{
public Android.Bluetooth.ProfileState State
{
get; internal set;
}
}
internal class CharacteristicEventArgs : GattEventArgs
{
public Android.Bluetooth.BluetoothGattCharacteristic Characteristic
{
get; internal set;
}
}
существоватьPlatformInitв методе Подключиться к серверу ГАТТ. Пользовательский обратный вызов Gatt Интегрировано из Android.Bluetooth.BluetoothGattCallback, из-за нехватки места здесь показана только переписанная версия одного метода FeatureWrite. Для реализации полной функции необходимо использовать как минимум четыре дополнительных метода ServicesDiscovered, ConnectionStateChanged, FeatureChanged, FeatureRead, DescriptorRead и DescriptorWrite. переписан. Подробности см. в исходном коде. Когда мы отправляем данные в значение характеристики устройства, будет запущен метод OnCharacteristicWrite, и внутри этого метода будет запущен наш собственный FeatureWrite.
В примерах официального документа нет примера написания значений характеристик, поэтому реализуем его здесь сами. Мы создаем новый класс GattCharacteristic, создаем GattCharacteristic.cs в корневом каталоге проекта, создаем GattCharacteristic.android.cs в каталоге Android и добавляем метод PlatformWriteValue в GattCharacteristic.android.cs.
Task PlatformWriteValue(byte[] value, bool requireResponse)
{
TaskCompletionSource<bool> tcs = null;
if (requireResponse)
{
tcs = new TaskCompletionSource<bool>();
void handler(object s, CharacteristicEventArgs e)
{
if (e.Characteristic == _characteristic)
{
Service.Device.Gatt.CharacteristicWrite -= handler;
if (!tcs.Task.IsCompleted)
{
tcs.SetResult(e.Status == Android.Bluetooth.GattStatus.Success);
}
}
};
Service.Device.Gatt.CharacteristicWrite += handler;
}
bool written = _characteristic.SetValue(value);
_characteristic.WriteType = requireResponse ? Android.Bluetooth.GattWriteType.Default : Android.Bluetooth.GattWriteType.NoResponse;
written = ((Android.Bluetooth.BluetoothGatt)Service.Device.Gatt).WriteCharacteristic(_characteristic);
if (written && requireResponse)
return tcs.Task;
return Task.CompletedTask;
}
Сохраните массив байтов, который необходимо отправить, в локальное хранилище значения характеристики через _characteristic.SetValue, а затем отправьте его на удаленный сервер Gatt через WriteCharacteristic. TaskCompletionSource здесь используется, главным образом, для преобразования асинхронного режима в синхронизированный. Атрибуты функции записи Android Bluetooth делятся на WRITE_TYPE_DEFAULT (запись) и WRITE_TYPE_NO_RESPONSE (запись без возврата). Параметр requireResponse указывает, нужно ли устройству выполнить возврат, результат, сохраненный в TaskCompletionSource, будет возвращен вызывающей стороне. в виде Задания. Добавляем метод WriteValueWithResponseAsync в GattCharacteristic, что означает запись и ожидание возврата.
public Task WriteValueWithResponseAsync(byte[] value)
{
ThrowOnInvalidValue(value);
return PlatformWriteValue(value, true);
}
private void ThrowOnInvalidValue(byte[] value)
{
if (value is null)
throw new ArgumentNullException("value");
if (value.Length > 512)
throw new ArgumentOutOfRangeException("value", "Attribute value cannot be longer than 512 bytes");
}
Поскольку Bluetooth ограничивает максимальную длину одной записи до 512, здесь мы выполняем проверку длины. При такой организационной структуре, когда мы добавляем код реализации других платформ, мы можем напрямую вызывать код реализации конкретной платформы, вызывая PlatformWriteValue. Если вы хотите выполнить запись в Bluetooth, вам, конечно, сначала необходимо найти идентификатор службы и идентификатор характеристического значения устройства Bluetooth. Поэтому мы продолжаем добавлять переопределение OnConnectionStateChange в GattCallback.
internal event EventHandler<GattEventArgs> ServicesDiscovered;
...
internal class GattCallback : Android.Bluetooth.BluetoothGattCallback
{
...
public override void OnConnectionStateChange(Android.Bluetooth.BluetoothGatt gatt, Android.Bluetooth.GattStatus status, Android.Bluetooth.ProfileState newState)
{
System.Diagnostics.Debug.WriteLine($"ConnectionStateChanged Status:{status} NewState:{newState}");
_remoteGattServer.ConnectionStateChanged?.Invoke(_remoteGattServer, new ConnectionStateEventArgs { Status = status, State = newState });
if (newState == Android.Bluetooth.ProfileState.Connected)
{
if (!_remoteGattServer._servicesDiscovered)
gatt.DiscoverServices();
}
else
{
_remoteGattServer.Device.OnGattServerDisconnected();
}
}
}
private async Task<bool> WaitForServiceDiscovery()
{
if (_servicesDiscovered)
return true;
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
void handler(object s, GattEventArgs e)
{
ServicesDiscovered -= handler;
if (!tcs.Task.IsCompleted)
{
tcs.SetResult(true);
}
};
ServicesDiscovered += handler;
return await tcs.Task;
}
Task PlatformConnect()
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
void handler(object s, ConnectionStateEventArgs e)
{
ConnectionStateChanged -= handler;
switch (e.Status)
{
case Android.Bluetooth.GattStatus.Success:
tcs.SetResult(e.State == Android.Bluetooth.ProfileState.Connected);
break;
default:
tcs.SetResult(false);
break;
}
}
ConnectionStateChanged += handler;
bool success = _gatt.Connect();
if (success)
{
if (IsConnected)
return Task.FromResult(true);
return tcs.Task;
}
else
{
ConnectionStateChanged -= handler;
return Task.FromException(new OperationCanceledException());
}
}
async Task<List<GattService>> PlatformGetPrimaryServices(BluetoothUuid? service)
{
var services = new List<GattService>();
await WaitForServiceDiscovery();
foreach (var serv in _gatt.Services)
{
// if a service was specified only add if service uuid is a match
if (serv.Type == Android.Bluetooth.GattServiceType.Primary && (!service.HasValue || service.Value == serv.Uuid))
{
services.Add(new GattService(Device, serv));
}
}
return services;
}
...
}
...
internal class GattEventArgs : EventArgs
{
public Android.Bluetooth.GattStatus Status
{
get; internal set;
}
}
Когда устройство подключается или отключается от устройства, будет запущен переписанный нами метод OnConnectionStateChange. Затем внутри метода мы определяем, является ли это состояние подключенным (ProfileState.Connected), и ищем его через DiscoverServices службы оборудования gatt. и информация о характеристических значениях и т. д. Метод PlatformGetPrimaryServices используется для поиска всех основных служб устройства BLE (используйте GattServiceType.Primary, чтобы определить, является ли это основной службой) и возврата списка GattService. Класс GattService является настраиваемым классом, а не из-за ограничений по пространству. все показано здесь.
public sealed partial class GattService
{
public Task<IReadOnlyList<GattCharacteristic>> GetCharacteristicsAsync()
{
return PlatformGetCharacteristics();
}
...
Конкретная реализация PlatformGetCharacteristics находится в некоторых классах, соответствующих этому типу платформы.
partial class GattService
{
private Task<IReadOnlyList<GattCharacteristic>> PlatformGetCharacteristics()
{
List<GattCharacteristic> characteristics = new List<GattCharacteristic>();
foreach (var characteristic in NativeService.Characteristics)
{
characteristics.Add(new GattCharacteristic(this, characteristic));
}
return Task.FromResult((IReadOnlyList<GattCharacteristic>)characteristics.AsReadOnly());
}
...
Посредством вышеописанной серии операций мы уже можем получить конкретные сервисы и конкретные значения характеристик этого устройства. Для устройств BLE большинство из них транслируются через атрибут Notify. Нам нужно включить монитор вещания. Позвольте мне обратиться к коду JAVA.
JAVA код
private BluetoothGatt bluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
Способ включения мониторинга трансляции — записать команду (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) в соответствующий дескриптор включения трансляции. Добавляем метод PlatformStartNotifications в GattCharacteristic.android.cs.
private async Task PlatformStartNotifications()
{
byte[] data;
if (_characteristic.Properties.HasFlag(Android.Bluetooth.GattProperty.Notify))
data = Android.Bluetooth.BluetoothGattDescriptor.EnableNotificationValue.ToArray();
else if (_characteristic.Properties.HasFlag(Android.Bluetooth.GattProperty.Indicate))
data = Android.Bluetooth.BluetoothGattDescriptor.EnableIndicationValue.ToArray();
else
return;
((Android.Bluetooth.BluetoothGatt)Service.Device.Gatt).SetCharacteristicNotification(_characteristic, true);
var descriptor = await GetDescriptorAsync(GattDescriptorUuids.ClientCharacteristicConfiguration);
await descriptor.WriteValueAsync(data);
}
Определите, поддерживается ли Notify здесь,Затем вызовите EnableNotificationValue, чтобы создать данные инструкции, которые включают прослушивание.,Затем получите дескриптор, соответствующий значению этой функции, с помощью GetDescriptorAsync.,Здесь все очень просто. Просто вызовите GetDescriptor соответствующего значения характеристики Android.,код здесь отображаться не будет. Если устройство BLE имеет атрибут уведомить,Тогда у него должен быть дескриптор,Открытие или закрытие уведомления должно контролироваться инструкциями по написанию дескриптора.,Все операции над собственными значениями тогда проходятWriteValueAsync->PlatformWriteValueосознать。
Task PlatformWriteValue(byte[] value)
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
void handler(object s, DescriptorEventArgs e)
{
if (e.Descriptor == _descriptor)
{
Characteristic.Service.Device.Gatt.DescriptorWrite -= handler;
if (!tcs.Task.IsCompleted)
{
tcs.SetResult(e.Status == Android.Bluetooth.GattStatus.Success);
}
}
};
Characteristic.Service.Device.Gatt.DescriptorWrite += handler;
bool written = _descriptor.SetValue(value);
written = ((Android.Bluetooth.BluetoothGatt)Characteristic.Service.Device.Gatt).WriteDescriptor(_descriptor);
if (written)
return tcs.Task;
return Task.FromException(new OperationCanceledException());
}
На этом этапе мы реализовали подключение устройства, получение основных значений службы и характеристики, запись данных и включение мониторинга уведомлений. Осталось прослушать изменения значений характеристики. произойдет изменение характеристики на удаленном устройстве (мы получим сообщение), сработает обратный вызов onCharacteristicChanged():
JAVAкод
@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}
Добавьте в GattCharacteristic.cs
void OnCharacteristicValueChanged(GattCharacteristicValueChangedEventArgs args)
{
characteristicValueChanged?.Invoke(this, args);
}
public event EventHandler<GattCharacteristicValueChangedEventArgs> CharacteristicValueChanged
{
add
{
characteristicValueChanged += value;
AddCharacteristicValueChanged();
}
remove
{
characteristicValueChanged -= value;
RemoveCharacteristicValueChanged();
}
}
...
public sealed class GattCharacteristicValueChangedEventArgs : EventArgs
{
internal GattCharacteristicValueChangedEventArgs(byte[] newValue)
{
Value = newValue;
}
public byte[] Value { get; private set; }
}
Добавьте GattCharacteristic.android.cs, соответствующий платформе.
void AddCharacteristicValueChanged()
{
Service.Device.Gatt.CharacteristicChanged += Gatt_CharacteristicChanged;
}
void RemoveCharacteristicValueChanged()
{
Service.Device.Gatt.CharacteristicChanged -= Gatt_CharacteristicChanged;
}
private void Gatt_CharacteristicChanged(object sender, CharacteristicEventArgs e)
{
if (e.Characteristic == _characteristic)
OnCharacteristicValueChanged(new GattCharacteristicValueChangedEventArgs(e.Characteristic.GetValue()));
}
Идея реализации здесь такая же, как и раньше.
Добавляем метод отправки данных в MasaMauiBluetoothService.
public async Task SendDataAsync(string deviceName,Guid servicesUuid,Guid? characteristicsUuid, byte[] dataBytes, EventHandler<GattCharacteristicValueChangedEventArgs> gattCharacteristicValueChangedEventArgs)
{
BluetoothDevice blueDevice = _discoveredDevices.FirstOrDefault(o => o.Name == deviceName);
var primaryServices = await blueDevice.Gatt.GetPrimaryServicesAsync();
var primaryService = primaryServices.First(o => o.Uuid.Value == servicesUuid);
var characteristics = await primaryService.GetCharacteristicsAsync();
var characteristic = characteristics.FirstOrDefault(o => (o.Properties & GattCharacteristicProperties.Write) != 0);
if (characteristicsUuid != null)
{
characteristic = characteristics.FirstOrDefault(o => o.Uuid.Value == characteristicsUuid);
}
await characteristic.StartNotificationsAsync();
characteristic.CharacteristicValueChanged += gattCharacteristicValueChangedEventArgs;
await characteristic.WriteValueWithResponseAsync(dataBytes);
}
существоватьMasa.Blazor.Maui.Plugin.BlueToothSampleпроект的Index.razor.csдобавить втесткод
public partial class Index
{
private string SelectedDevice;
private List<string> _allDeviceResponse = new List<string>();
[Inject]
private MasaMauiBluetoothService BluetoothService { get; set; }
...
private async Task SendDataAsync(string cmd= "AT+QVERSION")
{
var byteData = System.Text.Encoding.Default.GetBytes(cmd);
await SendDataAsync(SelectedDevice, byteData);
}
private async Task SendDataAsync(string deviceSerialNo, byte[] byteData)
{
if (byteData.Any())
{
_allDeviceResponse = new List<string>();
#if ANDROID
await BluetoothService.SendDataAsync(deviceSerialNo,Guid.Parse("0000ffff-0000-1000-8000-00805f9b34fb"),null, byteData, onCharacteristicChanged);
#endif
}
}
void onCharacteristicChanged(object sender, GattCharacteristicValueChangedEventArgs e)
{
var deviceResponse = System.Text.Encoding.Default.GetString(e.Value);
_allDeviceResponse.Add(deviceResponse);
InvokeAsync(() => { StateHasChanged(); });
}
}
Отправьте команду «AT+QVERSION», чтобы запросить номер версии на устройстве. Устройство возвращает информацию, полученную с помощью метода onCharacteristicChanged. Устройство возвращает двоичный массив, поэтому его необходимо преобразовать в строку для отображения. Просто напишите интерфейс для изменения компонента Index.razor Masa Blazor: Masa Blazor.
(https://www.masastack.com/blazor)
@page "/"
<MButton OnClick="ScanBLEDeviceAsync">Сканировать устройства Bluetooth</MButton>
<div class="text-center">
<MDialog @bind-Value="ShowProgress" Width="500">
<ChildContent>
<MCard>
<MCardTitle>
Поиск устройств Bluetooth
</MCardTitle>
<MCardText>
<MProgressCircular Size="40" Indeterminate Color="primary"></MProgressCircular>
</MCardText>
</MCard>
</ChildContent>
</MDialog>
</div>
@if (BluetoothDeviceList.Any())
{
<MSelect style="margin-top:10px"
Outlined
Items="BluetoothDeviceList"
ItemText="u=>u"
ItemValue="u=>u"
TItem="string"
TValue="string"
TItemValue="string"
@bind-Value="SelectedDevice"
OnSelectedItemUpdate="item => SelectedDevice = item">
</MSelect>
}
@if (!string.IsNullOrEmpty(SelectedDevice))
{
<MButton OnClick="() => SendDataAsync()">Отправить команду версии запроса</MButton>
}
@if (_allDeviceResponse.Any())
{
<MCard>
<MTextarea Value="@string.Join(' ',_allDeviceResponse)"></MTextarea>
</MCard>
}
Давайте посмотрим на эффект