Hello everyone, so I'm making a seizure detection device, i'll be using my arduino nano ble 33 sense rev2's accelerometer for this and so basically-- I'll be creating a phone app by flutter run in vscode that gets the data from the arduino if ever a seizure will be detected and sends an email with a google maps link to the input caretaker's email on the app screen using the phone's built in gps and sms. but i honestly have no idea how to make my app connect to the arduino via ble. should i change the andorid manifest xml?,
this is my current main.dart right now:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SeizureAlert BLE',
debugShowCheckedModeBanner: false,
// β
ABSOLUTE FINAL LICENSE FIX
theme: ThemeData(
useMaterial3: false, // Disable Material 3
primarySwatch: Colors.red,
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFB71C1C),
foregroundColor: Colors.white,
),
),
home: const BleMonitor(),
);
}
}
class BleMonitor extends StatefulWidget {
const BleMonitor({super.key});
@override
State<BleMonitor> createState() => _BleMonitorState();
}
class _BleMonitorState extends State<BleMonitor> {
BluetoothDevice? device;
BluetoothCharacteristic? charac;
String status = 'SCAN FOR NANO BLE 33';
String email = '';
bool isConnected = false;
StreamSubscription<List<int>>? subscription;
final TextEditingController _emailController = TextEditingController();
@override
void initState() {
super.initState();
loadEmail();
_emailController.addListener(() {
email = _emailController.text;
});
}
Future<void> loadEmail() async {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
email = prefs.getString('email') ?? '';
_emailController.text = email;
});
}
}
Future<void> saveEmail(String e) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('email', e);
}
Future<void> scan() async {
setState(() => status = 'Scanning...');
if (!await FlutterBluePlus.isSupported) {
setState(() => status = 'Bluetooth NOT supported');
return;
}
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 4));
FlutterBluePlus.scanResults.listen((results) async {
for (ScanResult r in results) {
debugPrint('Found: ${r.device.platformName}');
if (r.device.platformName.toLowerCase().contains('nano') ||
r.device.platformName.toLowerCase().contains('arduino') ||
r.device.platformName.toLowerCase().contains('ble')) {
await FlutterBluePlus.stopScan();
connect(r.device);
break;
}
}
});
}
Future<void> connect(BluetoothDevice d) async {
setState(() => status = 'Connecting...');
try {
await d.connect(timeout: const Duration(seconds: 10));
device = d;
final services = await d.discoverServices();
for (BluetoothService s in services) {
for (BluetoothCharacteristic c in s.characteristics) {
if (c.properties.notify) {
charac = c;
await c.setNotifyValue(true);
subscription = c.lastValueStream.listen((value) {
if (value.isNotEmpty && value[0] == 1) {
sendAlert();
}
});
if (mounted) {
setState(() {
isConnected = true;
status = 'π§ Monitoring Seizures';
});
}
return;
}
}
}
setState(() => status = 'No notify characteristic');
} catch (e) {
setState(() => status = 'Connection failed: $e');
}
}
Future<void> disconnect() async {
await device?.disconnect();
await subscription?.cancel();
if (mounted) {
setState(() {
isConnected = false;
device = null;
charac = null;
status = 'Disconnected';
});
}
}
Future<void> sendAlert() async {
if (email.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('β οΈ Enter caretaker email first!')),
);
}
return;
}
const String lat = "8.5853";
const String lon = "123.8425";
const String maps = "https://maps.google.com/?q=$lat,$lon";
final Uri uri = Uri(
scheme: 'mailto',
path: email,
queryParameters: {
'subject': 'π¨ SEIZURE ALERT - EMERGENCY',
'body':
'Seizure detected!\n\nπ Location: $lat,$lon\nπΊοΈ Maps: $maps\n\nEMERGENCY - CALL NOW!',
},
);
await launchUrl(uri);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('π¨ Alert sent to $email'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: const Text('π§ SeizureAlert BLE'),
backgroundColor: Colors.red[700],
foregroundColor: Colors.white,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(25),
decoration: BoxDecoration(
color: isConnected ? Colors.green[100] : Colors.orange[100],
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isConnected ? Colors.green : Colors.orange,
width: 2,
),
),
child: Column(
children: [
Icon(
isConnected
? Icons.monitor_heart
: Icons.bluetooth_searching,
size: 60,
color: isConnected ? Colors.green[700] : Colors.orange[700],
),
const SizedBox(height: 15),
Text(
status,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 40),
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Caretaker Email *',
hintText: 'emergency@example.com',
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
keyboardType: TextInputType.emailAddress,
onEditingComplete: () => saveEmail(_emailController.text),
),
const SizedBox(height: 40),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed: scan,
icon: const Icon(Icons.search),
label: const Text('SCAN'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 30,
vertical: 15,
),
),
),
ElevatedButton.icon(
onPressed: isConnected ? disconnect : null,
icon: const Icon(Icons.bluetooth_disabled),
label: const Text('DISCONNECT'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
),
),
],
),
const SizedBox(height: 20),
const Text(
'π Ozamiz City | Arduino Nano BLE 33 Sense Rev2',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
);
}
@override
void dispose() {
_emailController.dispose();
subscription?.cancel();
device?.disconnect();
super.dispose();
}
}
and this here is my pubspec yaml:
name: flutter_application_1
description: Seizure Alert BLE App
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.2.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_blue_plus: ^1.35.5
url_launcher: ^6.3.1
shared_preferences: ^2.3.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
this is my android manifest xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- BLE Permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Location (required for BLE scan) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Internet (for maps) -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="SeizureAlert"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
this is my build.gradle.kts:
android {
namespace = "com.example.seizure_alert"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
defaultConfig {
applicationId = "com.example.seizure_alert"
minSdk = 21 // β CHANGED FROM 26 TO 21
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
and this is my widget test dart:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('SeizureAlert BLE test', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('SCAN FOR NANO BLE 33', style: TextStyle(fontSize: 18)),
SizedBox(height: 20),
Text('Caretaker Email *'),
],
),
),
),
));
expect(find.text('SCAN FOR NANO BLE 33'), findsOneWidget);
expect(find.text('Caretaker Email *'), findsOneWidget);
});
}
help is very much appreciated because I always get this problem (
[{
"resource": "/c:/Users/ADMIN/Documents/sentryultrafinalnewest/flutter_application_1/lib/main.dart",
"owner": "_generated_diagnostic_collection_name_#0",
"code": {
"value": "missing_required_argument",
"target": {
"$mid": 1,
"path": "/diagnostics/missing_required_argument",
"scheme": "https",
"authority": "dart.dev"
}
},
"severity": 8,
"message": "The named parameter 'license' is required, but there's no corresponding argument.\nTry adding the required argument.",
"source": "dart",
"startLineNumber": 99,
"startColumn": 15,
"endLineNumber": 99,
"endColumn": 22,
"modelVersionId": 57,
"origin": "extHost1"
}]
no matter how many times i try to 'fix' it, that one problem always occurs. so help is vv much appreciated