execute method

Future<VtjCommandResult<TResult>> execute(
  1. BluetoothCharacteristic writeCharacteristic,
  2. BluetoothCharacteristic replyCharacteristic
)

Execute the command on the device.

This default implementation handles the common execution pattern:

  1. Validates using validate hook
  2. Clears stale data from the reply characteristic
  3. Sets up a stream listener filtered by commandId
  4. Writes the request and waits for response with timeout

Override this method only for commands with special execution needs (e.g., EnterDfuBootloaderCommand which expects disconnection).

Implementation

Future<VtjCommandResult<TResult>> execute(
  BluetoothCharacteristic writeCharacteristic,
  BluetoothCharacteristic replyCharacteristic,
) async {
  // Run validation hook
  final validationError = validate();
  if (validationError != null) return validationError;

  final completer = Completer<VtjCommandResult<TResult>>();
  late StreamSubscription subscription;

  try {
    await _clearStaleData(replyCharacteristic);

    subscription = replyCharacteristic.lastValueStream.listen((data) {
      if (data.isNotEmpty && data[0] == commandId) {
        final result = parseResponse(Uint8List.fromList(data));
        subscription.cancel();
        if (!completer.isCompleted) {
          completer.complete(result);
        }
      }
    });

    if (!replyCharacteristic.isNotifying) {
      await replyCharacteristic.setNotifyValue(true);
    }

    final request = buildRequest();
    await writeCharacteristic.write(request, withoutResponse: false);

    return await completer.future.timeout(
      timeout,
      onTimeout: () {
        subscription.cancel();
        return const VtjCommandResult.failure('Command timeout', 408);
      },
    );
  } catch (e) {
    subscription.cancel();
    if (!completer.isCompleted) {
      return VtjCommandResult.failure('Command failed: $e', null);
    }
    return completer.future;
  }
}