vtj_bluetooth_package 1.0.6 copy "vtj_bluetooth_package: ^1.0.6" to clipboard
vtj_bluetooth_package: ^1.0.6 copied to clipboard

A Flutter package for seamless Bluetooth Low Energy (BLE) communication with Ventriject medical devices.

Ventriject Bluetooth Package #

pub package License: MIT

A Flutter package that provides seamless Bluetooth Low Energy (BLE) communication with Ventriject Seismofit medical devices. This package handles device discovery, connection management, authentication, measurement recording, and data transfer using Ventriject's proprietary BLE protocol.

Table of Contents #

Features #

  • 🔍 Device Discovery - Automatic filtering and validation of Ventriject devices via BLE advertising data
  • Advertising Data Parsing - Battery level, device state, and MAC address available before connection
  • Device Authentication - Secure key-based authentication
  • 📊 Measurement Recording - Configurable accelerometer measurements with progress tracking
  • 📥 Data Transfer - Download measurement data with CRC validation and progress updates
  • 💡 Device Identification - LED blink pattern to identify physical device
  • 🔄 DFU Support - Enter bootloader mode for firmware updates
  • 📱 Platform Support - Android and iOS

Getting Started #

Installation #

Add the package to your pubspec.yaml:

dependencies:
  vtj_bluetooth_package: ^1.0.0

Android Configuration #

Add these permissions to your AndroidManifest.xml:

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation"
    tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

iOS Configuration #

Add these entries to your Info.plist:

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Need BLE permission to connect to Ventriject devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Need BLE permission to connect to Ventriject devices</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Need location permission to scan for BLE devices</string>

Architecture Overview #

The package follows a repository pattern with two main components:

import 'package:vtj_bluetooth_package/vtj_bluetooth_package.dart';

// Create the datasource (handles low-level BLE communication)
final datasource = BluePlusVtjDatasource();

// Create the repository (provides high-level API)
final repository = VtjDevicesRepositoryImpl(datasource);

All repository methods return Either<VtjFailure, T> for error handling using functional programming patterns from the fpdart package.


Device Discovery #

Starting a Scan #

// Start scanning for Ventriject devices
final scanResult = await repository.startScan();

scanResult.fold(
  (failure) => print('Failed to start scan: ${failure.message}'),
  (_) => print('Scan started successfully'),
);

Listening to Scan Results #

The getScanResults() method returns a stream of discovered devices. The package automatically filters and validates Ventriject devices based on advertising data.

final subscription = repository.getScanResults().listen((result) {
  result.fold(
    (failure) => print('Scan error: ${failure.message}'),
    (devices) {
      for (final device in devices) {
        print('Found: ${device.name}');
        print('  MAC Address: ${device.macAddress}');
        print('  Battery: ${device.batteryPercentage}%');
        print('  State: ${device.state.displayName}');
        print('  Available: ${device.isAvailable}');
      }
    },
  );
});

// Cancel subscription when done
await subscription.cancel();

VtjDevice Properties #

Each discovered device (VtjDevice) contains:

Property Type Description
device BluetoothDevice Underlying Flutter Blue Plus device
advertisingData VtjAdvertisingData Parsed advertising data
name String Device name (e.g., "VTJ-XXXX")
macAddress String MAC address (e.g., "AA:BB:CC:DD:EE:FF")
batteryPercentage int Battery level 0-100%
state DeviceState Current device state
isAvailable bool Whether device can be connected (not busy)
firmwareVersion String? Firmware version (null until connected)

Device States (from Advertising) #

State Description Can Connect?
DeviceState.ready No measurement, no data ✅ Yes
DeviceState.busy Recording in progress ❌ No
DeviceState.dataReady Has measurement data ✅ Yes

Connection Management #

Connecting to a Device #

final result = await repository.connectToDevice(vtjDevice);

result.fold(
  (failure) => print('Connection failed: ${failure.message}'),
  (connectedDevice) {
    print('Connected to ${connectedDevice.name}');
    // Now you can execute commands using connectedDevice.device
  },
);

Disconnecting #

final result = await repository.disconnect(device.device);

result.fold(
  (failure) => print('Disconnect failed: ${failure.message}'),
  (_) => print('Disconnected successfully'),
);

Authentication #

After connecting, you must authenticate before executing most commands.

// Device key is provided with each Ventriject device
const deviceKey = 'your-device-key';

final result = await repository.authenticateDevice(device.device, deviceKey);

result.fold(
  (failure) => print('Authentication failed: ${failure.message}'),
  (success) {
    if (success) {
      print('Authentication successful');
    } else {
      print('Authentication failed: Invalid key');
    }
  },
);

Note: Authentication is required before using commands like startMeasurement, fetchMeasurement, deleteMeasurement, etc.


Device Commands #

Get Version #

Retrieves the firmware version of the connected device.

final result = await repository.getVersion(device.device);

result.fold(
  (error) => print('Error: $error'),
  (versionResult) => print('Firmware: ${versionResult.version}'),
);

Get Device State #

Retrieves detailed state information about the device and any stored measurement.

final result = await repository.getState(device.device);

result.fold(
  (failure) => print('Error: ${failure.message}'),
  (state) {
    print('State: ${state.stateDescription}');
    print('Has Data: ${state.hasData}');
    print('Is Recording: ${state.isRecording}');
    print('Is Idle: ${state.isIdle}');
    
    // These return null when no valid measurement exists
    if (state.hasData) {
      print('Measurement Length: ${state.validMeasurementLength} seconds');
      print('Size: ${state.validSize} bytes');
      print('CRC: 0x${state.validCrc?.toRadixString(16)}');
    }
  },
);

GetDeviceStateResult Properties

Property Type Description
measurementState int Raw state value (0, 1, or 2)
stateDescription String Human-readable state
hasData bool True if measurement data is ready
isRecording bool True if measurement in progress
isIdle bool True if no measurement/data
validMeasurementLength int? Duration in seconds (null if no data)
validTotalLength int? Duration in 100ths of seconds (null if no data)
validSize int? Data size in bytes (null if no data)
validCrc int? CRC checksum (null if no data)

Makes the device LED blink to identify the physical device.

final result = await repository.identifyBlink(device.device);

result.fold(
  (failure) => print('Error: ${failure.message}'),
  (blinkResult) => blinkResult.when(
    success: () => print('Device LED is blinking'),
    failure: (message, code) => print('Failed: $message'),
  ),
);

Delete Measurement #

Deletes any stored measurement data from the device.

final result = await repository.deleteMeasurement(device.device);

result.fold(
  (failure) => print('Error: ${failure.message}'),
  (deleteResult) => deleteResult.when(
    success: () => print('Measurement deleted'),
    failure: (message, code) => print('Failed: $message (code: $code)'),
  ),
);

Enter DFU Bootloader #

Reboots the device into DFU (Device Firmware Update) bootloader mode for firmware updates.

final result = await repository.enterDfuBootloader(device.device);

result.fold(
  (failure) => print('Error: ${failure.message}'),
  (dfuResult) => dfuResult.when(
    success: () => print('Device entering DFU mode...'),
    failure: (message, code) => print('Failed: $message'),
  ),
);

Warning: After entering DFU mode, the device will disconnect and reappear as a DFU target.


Measurements #

Measurement Configuration #

Configure measurements using MeasurementConfig:

final config = MeasurementConfig(
  durationSeconds: 15,                        // 1-600 seconds
  measurementType: MeasurementType.hz1100,    // Sample rate
  filterType: FilterType.fir,                 // Filter type
  axesType: AxesType.zOnly,                   // Which axes to record
  dataFormat: DataFormat.compressed,          // Storage format
);

MeasurementType Options

Type Sample Rate Max Duration Bandwidth
MeasurementType.hz550 550 Hz 30 seconds 235 Hz
MeasurementType.hz1100 1100 Hz 15 seconds 440 Hz

FilterType Options

Type Description
FilterType.fir FIR filter (Linear Phase) - Recommended
FilterType.iir IIR filter (Non-Linear Phase)

AxesType Options

Type Description Data Size
AxesType.zOnly Only Z-axis Smaller
AxesType.xyz All three axes (X, Y, Z) 3x larger

DataFormat Options

Type Description
DataFormat.uncompressed Raw sample data
DataFormat.compressed Delta-encoded compression

Starting a Measurement #

// 1. First, subscribe to measurement progress BEFORE starting
final progressSubscription = repository.measurementProgressStream(device.device)
    .listen((progress) {
  progress.when(
    idle: () => print('Measurement idle'),
    inProgress: (elapsed, total, fraction) {
      print('Recording: $elapsed / $total seconds (${(fraction! * 100).toInt()}%)');
    },
    completed: (totalSeconds) {
      print('Measurement completed! Duration: $totalSeconds seconds');
    },
    failed: (message, code, elapsed) {
      print('Measurement failed: $message');
    },
  );
});

// 2. Create configuration
final config = MeasurementConfig(
  durationSeconds: 15,
  measurementType: MeasurementType.hz1100,
  filterType: FilterType.fir,
  axesType: AxesType.zOnly,
  dataFormat: DataFormat.compressed,
);

// 3. Start measurement
final result = await repository.startMeasurement(device.device, config);

result.fold(
  (failure) => print('Failed to start: ${failure.message}'),
  (startResult) => startResult.when(
    success: () => print('Measurement started!'),
    failure: (message, code) => print('Error: $message (code: $code)'),
  ),
);

// 4. Clean up when done
await progressSubscription.cancel();
repository.resetMeasurementState(device.device);

Tracking Measurement Progress #

The MeasurementProgress class has four states:

progress.when(
  idle: () {
    // Initial state or after reset
  },
  inProgress: (int elapsedSeconds, int totalSeconds, double? fraction) {
    // Recording in progress
    // fraction is 0.0 to 1.0 for progress bar
  },
  completed: (int totalSeconds) {
    // Recording finished successfully
    // Data is now stored on device
  },
  failed: (String message, int? code, int? elapsedSeconds) {
    // Recording failed
  },
);

MeasurementProgress Helper Properties

Property Type Description
isActive bool True if currently recording
isDone bool True if completed or failed
isSuccess bool True if completed successfully

Stopping a Measurement #

To prematurely stop an ongoing measurement:

final result = await repository.stopMeasurement(device.device);

result.fold(
  (failure) => print('Error: ${failure.message}'),
  (stopResult) => stopResult.when(
    success: () => print('Measurement stopped'),
    failure: (message, code) => print('Failed: $message'),
  ),
);

Data Transfer #

Fetching Measurement Data #

After a measurement completes, download the data from the device.

// 1. Subscribe to fetch progress BEFORE starting fetch
final fetchSubscription = repository.fetchProgressStream(device.device)
    .listen((progress) {
  progress.when(
    idle: () => print('Fetch idle'),
    inProgress: (received) {
      print('Downloading... $received bytes received');
    },
    completed: (Uint8List data, int totalBytes, bool crcValid) {
      print('Download complete!');
      print('  Size: $totalBytes bytes');
      print('  CRC Valid: $crcValid');
      
      if (crcValid) {
        // Process the measurement data
        processData(data);
      } else {
        print('WARNING: CRC validation failed - data may be corrupted');
      }
    },
    failed: (String message, int? code, int? receivedBytes) {
      print('Download failed: $message');
      if (receivedBytes != null) {
        print('  Received before failure: $receivedBytes bytes');
      }
    },
  );
});

// 2. Start fetch
final result = await repository.fetchMeasurement(device.device);

result.fold(
  (failure) => print('Failed to start fetch: ${failure.message}'),
  (fetchResult) => fetchResult.when(
    success: () => print('Fetch started'),
    failure: (message, code) => print('Error: $message'),
  ),
);

// 3. Clean up when done
await fetchSubscription.cancel();
repository.resetFetchState(device.device);

Tracking Fetch Progress #

The FetchProgress class has four states:

progress.when(
  idle: () {
    // Initial state or after reset
  },
  inProgress: (int receivedBytes) {
    // Download in progress
    // Shows bytes received so far (use a spinner, not progress bar)
  },
  completed: (Uint8List data, int totalBytes, bool crcValid) {
    // Download finished
    // data contains the raw measurement bytes
    // crcValid indicates if data integrity check passed
  },
  failed: (String message, int? code, int? receivedBytes) {
    // Download failed
    // receivedBytes can be used for resume functionality
  },
);

Note: Fetch completion is detected when the device sends an empty array (per BLE spec), not by tracking expected bytes. Use a spinner for UI feedback during download.

FetchProgress Helper Properties

Property Type Description
isActive bool True if currently downloading
isDone bool True if completed or failed
isSuccess bool True if completed successfully

Resuming a Fetch #

If a fetch is interrupted, you can resume from where it left off:

// Resume from byte 5000
final result = await repository.fetchMeasurement(
  device.device,
  startIndex: 5000,
);

Stopping a Fetch #

To cancel an ongoing download:

final result = await repository.stopFetchMeasurement(device.device);

result.fold(
  (failure) => print('Error: ${failure.message}'),
  (stopResult) => stopResult.when(
    success: () => print('Fetch stopped'),
    failure: (message, code) => print('Failed: $message'),
  ),
);

Error Handling #

All repository methods return Either<VtjFailure, T>:

final result = await repository.connectToDevice(device);

// Using fold
result.fold(
  (failure) {
    // Handle failure
    print('Error: ${failure.message}');
  },
  (success) {
    // Handle success
  },
);

// Using pattern matching
result.when(
  left: (failure) => handleError(failure),
  right: (success) => handleSuccess(success),
);

// Using isLeft/isRight
if (result.isLeft()) {
  final failure = result.getLeft().getOrElse(() => throw Exception());
  print('Failed: ${failure.message}');
}

Common VtjFailure Types #

Failure Description
VtjFailure.scanningError() BLE scanning failed
VtjFailure.connectionFailed() Could not connect to device
VtjFailure.commandFailed() BLE command execution failed
VtjFailure.timeout() Operation timed out
VtjFailure.permissionDenied() Missing BLE/location permissions

Troubleshooting #

Common Issues #

  1. Bluetooth not enabled:

    if (await FlutterBluePlus.isAvailable) {
      // Bluetooth is available
    }
       
    if (await FlutterBluePlus.isOn) {
      // Bluetooth is turned on
    }
    
  2. Location Services: On Android, ensure location services are enabled for BLE scanning.

  3. Permissions: Always check and request necessary permissions before scanning:

    // Check and request permissions before scanning
    // Use permission_handler package
    
  4. Device shows as "Busy": The device is currently recording. Wait for measurement to complete or power cycle the device.

  5. Authentication fails: Verify the device key is correct. Each device has a unique key.

  6. CRC validation fails: Data corruption during transfer. Try fetching again.

  7. Fetch incomplete: BLE connection may have dropped. Reconnect and resume from last received byte.

Debug Logging #

Enable Flutter Blue Plus logging for debugging:

FlutterBluePlus.setLogLevel(LogLevel.verbose);

Complete Workflow Example #

import 'package:vtj_bluetooth_package/vtj_bluetooth_package.dart';

Future<void> completeWorkflow() async {
  final datasource = BluePlusVtjDatasource();
  final repository = VtjDevicesRepositoryImpl(datasource);
  
  // 1. Scan for devices
  await repository.startScan();
  
  VtjDevice? targetDevice;
  final scanSub = repository.getScanResults().listen((result) {
    result.fold(
      (f) => print('Scan error'),
      (devices) {
        if (devices.isNotEmpty && targetDevice == null) {
          targetDevice = devices.first;
        }
      },
    );
  });
  
  // Wait for device discovery
  await Future.delayed(Duration(seconds: 5));
  await scanSub.cancel();
  
  if (targetDevice == null) {
    print('No device found');
    return;
  }
  
  // 2. Connect
  final connectResult = await repository.connectToDevice(targetDevice!);
  if (connectResult.isLeft()) {
    print('Connection failed');
    return;
  }
  
  final device = targetDevice!.device;
  
  // 3. Authenticate
  final authResult = await repository.authenticateDevice(device, 'your-key');
  if (authResult.isLeft() || !authResult.getOrElse(() => false)) {
    print('Authentication failed');
    return;
  }
  
  // 4. Check device state
  final stateResult = await repository.getState(device);
  stateResult.fold(
    (f) => print('Error getting state'),
    (state) {
      if (state.hasData) {
        print('Device has existing data - delete or fetch first');
      }
    },
  );
  
  // 5. Start measurement with progress tracking
  final measureSub = repository.measurementProgressStream(device).listen((p) {
    p.when(
      idle: () {},
      inProgress: (e, t, f) => print('Recording: $e/$t sec'),
      completed: (t) => print('Recording done!'),
      failed: (m, c, e) => print('Recording failed: $m'),
    );
  });
  
  final config = MeasurementConfig(
    durationSeconds: 10,
    measurementType: MeasurementType.hz550,
  );
  
  await repository.startMeasurement(device, config);
  
  // Wait for completion
  await Future.delayed(Duration(seconds: 12));
  await measureSub.cancel();
  
  // 6. Fetch data with progress tracking
  final fetchSub = repository.fetchProgressStream(device).listen((p) {
    p.when(
      idle: () {},
      inProgress: (received) => print('Downloading... $received bytes received'),
      completed: (data, size, valid) {
        print('Downloaded $size bytes, CRC valid: $valid');
        // Process data here
      },
      failed: (m, c, r) => print('Download failed: $m'),
    );
  });
  
  await repository.fetchMeasurement(device);
  
  // Wait for download
  await Future.delayed(Duration(seconds: 30));
  await fetchSub.cancel();
  
  // 7. Disconnect
  await repository.disconnect(device);
  
  print('Workflow complete!');
}

License #

This project is licensed under the MIT License - see the LICENSE file for details.

Support #

For support, please open an issue in the GitHub repository or contact support@ventriject.com.

1
likes
150
points
39
downloads

Documentation

API reference

Publisher

verified publisherventriject.com

Weekly Downloads

A Flutter package for seamless Bluetooth Low Energy (BLE) communication with Ventriject medical devices.

Homepage
Repository

License

MIT (license)

Dependencies

crypto, device_info_plus, dio, flutter, flutter_blue_plus, fpdart, freezed_annotation, intl, json_annotation, meta, permission_handler, retrofit, shared_preferences, uuid

More

Packages that depend on vtj_bluetooth_package