Плагин MASA MAUI Android Bluetooth с низким энергопотреблением (2) Bluetooth-связь
Плагин MASA MAUI Android Bluetooth с низким энергопотреблением (2) Bluetooth-связь

Предыстория проекта

Появление 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.

Язык кода:javascript
копировать
JAVAкод
bluetoothGatt = device.connectGatt(this, false, gattCallback);

Чтобы подключиться к серверу GATT на устройстве BLE, вам необходимо использовать метод ConnectGatt(). Этот метод принимает три параметра: объект Context, autoConnect (логическое значение, указывающее, следует ли автоматически подключаться к устройству BLE, если оно доступно) и ссылку на BluetoothGattCallback. Этот метод извлекает экземпляр BluetoothGatt, который затем можно использовать для выполнения клиентских операций GATT. Вызывающий абонент (приложение Android) является клиентом GATT. BluetoothGattCallback используется для передачи результатов (например, состояния соединения) клиенту, а также любых дальнейших клиентских операций GATT. Давайте еще раз взглянем на JAVA-реализацию BluetoothGattCallback.

Язык кода:javascript
копировать
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.

Язык кода:javascript
копировать
    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.

Напишите команду Bluetooth

В примерах официального документа нет примера написания значений характеристик, поэтому реализуем его здесь сами. Мы создаем новый класс GattCharacteristic, создаем GattCharacteristic.cs в корневом каталоге проекта, создаем GattCharacteristic.android.cs в каталоге Android и добавляем метод PlatformWriteValue в GattCharacteristic.android.cs.

Язык кода:javascript
копировать
        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, что означает запись и ожидание возврата.

Язык кода:javascript
копировать
        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.

Язык кода:javascript
копировать
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 является настраиваемым классом, а не из-за ограничений по пространству. все показано здесь.

Язык кода:javascript
копировать
  public sealed partial class GattService
    {
        public Task<IReadOnlyList<GattCharacteristic>> GetCharacteristicsAsync()
        {
            return PlatformGetCharacteristics();
        }
        ...

Конкретная реализация PlatformGetCharacteristics находится в некоторых классах, соответствующих этому типу платформы.

Язык кода:javascript
копировать
    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());
        }
        ...

Включите мониторинг Bluetooth

Посредством вышеописанной серии операций мы уже можем получить конкретные сервисы и конкретные значения характеристик этого устройства. Для устройств BLE большинство из них транслируются через атрибут Notify. Нам нужно включить монитор вещания. Позвольте мне обратиться к коду JAVA.

Язык кода:javascript
копировать
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.

Язык кода:javascript
копировать
  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осознать。

Язык кода:javascript
копировать
        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():

Язык кода:javascript
копировать
JAVAкод
@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
        BluetoothGattCharacteristic characteristic) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}

Добавьте в GattCharacteristic.cs

Язык кода:javascript
копировать
        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, соответствующий платформе.

Язык кода:javascript
копировать
        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.

Язык кода:javascript
копировать
        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добавить втесткод

Язык кода:javascript
копировать
 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)

Язык кода:javascript
копировать
@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>
}

Давайте посмотрим на эффект

boy illustration
Лучшая практика Docker: Docker развертывает практику Kibana с одним узлом
boy illustration
Создайте свой собственный сетевой диск — идеальное решение для персонального облачного хранилища — nextcloud AIO (2)
boy illustration
Облачные вычисления, «облако» вокруг людей
boy illustration
API рендеринга изображений: быстро создавайте изображения для электронной коммерции, социальных сетей, маркетинга, баннеров, сертификатов и т. д.!
boy illustration
Сколько стоит облачный настольный сервер? Выгодно ли использовать облачный рабочий стол?
boy illustration
Spring Boot и Kubernetes: идеальное сочетание для современного облачного развертывания
boy illustration
Первый опыт работы с отечественной базой данных TiDB
boy illustration
Observable Platform-1: анализ выбора технологии
boy illustration
Последняя версия руководства по регистрации и покупке хоста Hostinger 2023 года
boy illustration
Классификация виртуализации и обзор технологии виртуализации ввода-вывода
boy illustration
Анализ технологии веб-сервисов XML: краткий обзор принципов WSDL и SOAP и случаев применения
boy illustration
Лучшие практики KubeSphere: 14 больших изображений в высоком разрешении, которые помогут вам сначала познакомиться с KubeSphere v4.1.1, полное руководство по развертыванию AIO.
boy illustration
Говоря об онлайн-обучении K12, мы хотим сесть и поговорить с вами об этих вещах.
boy illustration
Потребление памяти K8S, какую картинку посмотреть?
boy illustration
Облачное развертывание редактора документов etherpad с использованием Docker
boy illustration
Модель интегрированного языка: Edge SLM + Cloud LLM
boy illustration
Использование набора тестов производительности phoronix-test-suite в Windows
boy illustration
Как ПВХ связан с PV
boy illustration
Конфигурация построения распределенной файловой системы seaweedfs
boy illustration
Обзор платформы управления метаданными DataHub
boy illustration
Terraform: реализация инфраструктуры как кода в мультиоблачных и гибридных облачных средах
boy illustration
Используйте Rclone для переноса данных в MinIO
boy illustration
Enterprise WeChat, еще одно «поле битвы» для Double 11
boy illustration
Подробное руководство по созданию личного блога с использованием облачного сервера + WordPress.
boy illustration
Основы обучения серии napi — как разработать проект NAPI с помощью DevEco Studio
boy illustration
Консолидируйте и удалите кластер Kubernetes: изучите RKE2 и системный агент Rancher
boy illustration
[Машинное обучение] Dify: обновление версии платформы разработки агентов искусственного интеллекта
boy illustration
Вот как должна быть устроена платежная система, чтобы она была стабильной! !
boy illustration
Лучшая практика Docker: настройка универсального шаблона Docker для создания
boy illustration
Изучите ES от входа до практики