healthrian_ble_support_for_cardio_em_cdc_http 0.0.5
healthrian_ble_support_for_cardio_em_cdc_http: ^0.0.5 copied to clipboard
BLE support for Cardio EM CDC over HTTP
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:healthrian_ble_interface/healthrian_ble_interface.dart';
import 'package:healthrian_ble_support_for_cardio_em_cdc_http/healthrian_ble_support_for_cardio_em_cdc_http.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await filter.waitForCompletion();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cardio EM CDC HTTP Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _topicController = TextEditingController(text: 'CardioEMCDC/5019F820');
final TextEditingController _hostController = TextEditingController(text: '127.0.0.1');
final TextEditingController _portController = TextEditingController(text: '8080');
BLEManager? _manager;
void _setupManager() {
final topic = _topicController.text;
final host = _hostController.text;
final port = int.tryParse(_portController.text) ?? 8080;
setState(() {
_manager = BLEManager(host: host, port: port)..topic = topic;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Cardio EM CDC HTTP Example'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextField(
controller: _hostController,
decoration: const InputDecoration(labelText: 'Host'),
),
TextField(
controller: _portController,
decoration: const InputDecoration(labelText: 'Port'),
keyboardType: TextInputType.number,
),
TextField(
controller: _topicController,
decoration: const InputDecoration(labelText: 'Topic'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _setupManager,
child: const Text('Setup Manager'),
),
],
),
),
),
),
if (_manager != null) ...[
Expanded(
child: ListView(
children: [
ListTile(
title: const Text('Status'),
subtitle: Observer(
builder: (_) => StreamBuilder<bool?>(
stream: _manager!.ble.streams.ble.connection,
builder: (context, snapshot) {
return Text('Connected: ${snapshot.data ?? false}');
},
),
),
),
ListTile(
title: const Text('Is Measuring'),
subtitle: Observer(
builder: (_) => Text(_manager!.ecg?.isMeasuring.toString() ?? 'false'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: 200,
child: Card(
child: Observer(
builder: (_) => StreamBuilder<ChannelData>(
stream: _manager!.ble.streams.ecg.filter.plot,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: Text('No Data'));
}
return CustomPaint(
painter: EcgPainter(snapshot.data!),
);
},
),
),
),
),
),
ListTile(
title: const Text('Raw Data (Hex)'),
subtitle: Observer(
builder: (_) => StreamBuilder<List<int>>(
stream: _manager!.ble.streams.ecg.meta.raw,
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text('N/A');
final hex = snapshot.data!.take(16).map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ');
return Text(hex);
},
),
),
),
],
),
),
],
],
),
floatingActionButton: _manager != null ? Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => _manager!.connectAndRegister(),
heroTag: 'connect',
child: const Icon(Icons.link),
),
const SizedBox(height: 10),
FloatingActionButton(
onPressed: () => _manager!.ecg?.start(),
heroTag: 'play',
child: const Icon(Icons.play_arrow),
),
const SizedBox(height: 10),
FloatingActionButton(
onPressed: () => _manager!.ecg?.stop(),
heroTag: 'stop',
child: const Icon(Icons.stop),
),
const SizedBox(height: 10),
FloatingActionButton(
onPressed: () => _manager!.disconnectDevice(),
heroTag: 'disconnect',
child: const Icon(Icons.link_off),
),
],
) : null,
);
}
}
class EcgPainter extends CustomPainter {
final ChannelData data;
EcgPainter(this.data);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.red
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
final l2 = data.l2;
if (l2.isEmpty) return;
final path = Path();
final double stepX = size.width / data.limit;
// We need to handle the circular buffer using the pivot
for (int i = 0; i < l2.length; i++) {
// Simple visualization for now: just draw what's in the buffer
// The ChannelData.plot stream already provides the circular buffer data
// but we might need to handle the 'gap' if we want it to look exactly like a monitor.
// For a simple example, we'll just draw the lines.
double x = i * stepX;
// Scale Y: assume values are around 0 with some range.
// This is a rough scaling for visualization.
double y = size.height / 2 - (l2[i] / 1000);
if (i == 0) {
path.moveTo(x, y);
} else {
// Handle the jump at the pivot if we are drawing the full limit
if (i == data.pivot) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
}
canvas.drawPath(path, paint);
// Draw the cursor at the pivot
final cursorX = data.pivot * stepX;
canvas.drawLine(Offset(cursorX, 0), Offset(cursorX, size.height), Paint()..color = Colors.blue.withValues(alpha: 0.5)..strokeWidth = 2);
}
@override
bool shouldRepaint(covariant EcgPainter oldDelegate) => true;
}