initial
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/ble_service.dart';
|
||||
import '../siab_config.dart';
|
||||
|
||||
class ConnectionCard extends StatelessWidget {
|
||||
final BLEService ble;
|
||||
|
||||
const ConnectionCard({super.key, required this.ble});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
ble.isConnected
|
||||
? Icons.bluetooth_connected
|
||||
: Icons.bluetooth_disabled,
|
||||
color: ble.isConnected ? Colors.green : Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
ble.isConnected ? 'Connected' : 'Disconnected',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
if (ble.isScanning || ble.isConnecting)
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (ble.isConnected) ...[
|
||||
const SizedBox(height: 8),
|
||||
if (ble.deviceName != null)
|
||||
Text('Device: ${ble.deviceName}'),
|
||||
if (ble.deviceAddress != null)
|
||||
Text(
|
||||
'Address: ${ble.deviceAddress}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
if (!ble.isConnected && !ble.isScanning && !ble.isConnecting) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final success = await ble.autoConnect();
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Failed to find ${SiabConfig.deviceName} device',
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
label: const Text('Scan & Connect'),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/ble_service.dart';
|
||||
|
||||
class DataPacketCard extends StatelessWidget {
|
||||
final DataPacket packet;
|
||||
|
||||
const DataPacketCard({super.key, required this.packet});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final timeStr = '${packet.timestamp.hour.toString().padLeft(2, '0')}:'
|
||||
'${packet.timestamp.minute.toString().padLeft(2, '0')}:'
|
||||
'${packet.timestamp.second.toString().padLeft(2, '0')}';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: ExpansionTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
child: Text(
|
||||
'#${packet.packetNumber}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
packet.hexString,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 14),
|
||||
),
|
||||
subtitle: Text('$timeStr • ${packet.data.length} bytes'),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSection('Hex', packet.hexString),
|
||||
const SizedBox(height: 8),
|
||||
// 'timbang' style: bytes shown in decimal (matching Python output)
|
||||
_buildSection('timbang', packet.bytesString),
|
||||
const SizedBox(height: 16),
|
||||
if (packet.value32BitUnsigned != null) ...[
|
||||
_buildValueRow(
|
||||
'32-bit Unsigned (BE)',
|
||||
packet.value32BitUnsigned.toString(),
|
||||
),
|
||||
_buildValueRow(
|
||||
'32-bit Signed (BE)',
|
||||
packet.value32BitSigned.toString(),
|
||||
),
|
||||
if (packet.value32BitUnsigned! > 0) ...[
|
||||
_buildValueRow(
|
||||
'As Float (÷100)',
|
||||
(packet.value32BitUnsigned! / 100.0).toStringAsFixed(2),
|
||||
),
|
||||
_buildValueRow(
|
||||
'As Float (÷1000)',
|
||||
(packet.value32BitUnsigned! / 1000.0).toStringAsFixed(3),
|
||||
),
|
||||
],
|
||||
],
|
||||
if (packet.value16BitUnsigned != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildValueRow(
|
||||
'16-bit Unsigned (BE)',
|
||||
packet.value16BitUnsigned.toString(),
|
||||
),
|
||||
_buildValueRow(
|
||||
'16-bit Signed (BE)',
|
||||
packet.value16BitSigned.toString(),
|
||||
),
|
||||
],
|
||||
if (packet.asciiString != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('ASCII', packet.asciiString!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSection(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SelectableText(
|
||||
value,
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildValueRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/ble_service.dart';
|
||||
|
||||
class StatisticsCard extends StatelessWidget {
|
||||
final BLEService ble;
|
||||
|
||||
const StatisticsCard({super.key, required this.ble});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = ble.streamingDuration;
|
||||
final durationStr = duration != null
|
||||
? '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'
|
||||
: '0:00';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_StatItem(
|
||||
icon: Icons.data_usage,
|
||||
label: 'Packets',
|
||||
value: '${ble.packetCount}',
|
||||
),
|
||||
_StatItem(
|
||||
icon: Icons.speed,
|
||||
label: 'Rate',
|
||||
value: '${ble.packetsPerSecond.toStringAsFixed(2)}/s',
|
||||
),
|
||||
_StatItem(
|
||||
icon: Icons.timer,
|
||||
label: 'Duration',
|
||||
value: durationStr,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _StatItem({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 24, color: Theme.of(context).primaryColor),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user