This commit is contained in:
2026-01-29 16:17:31 +07:00
commit 363f113121
145 changed files with 7746 additions and 0 deletions
+79
View File
@@ -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'),
),
),
],
],
),
),
);
}
}
+126
View File
@@ -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,
),
),
],
),
);
}
}
+76
View File
@@ -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,
),
],
);
}
}