import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/ble_service.dart'; class CLIScreen extends StatefulWidget { const CLIScreen({super.key}); @override State createState() => _CLIScreenState(); } class _CLIScreenState extends State { final ScrollController _scrollController = ScrollController(); final List _logLines = []; Timer? _statusTimer; bool _autoScroll = true; int _lastProcessedPacket = 0; @override void initState() { super.initState(); _addHeader(); _startStatusTimer(); } @override void dispose() { _statusTimer?.cancel(); _scrollController.dispose(); super.dispose(); } String _repeatChar(String char, int count) { return List.filled(count, char).join(); } void _addHeader() { _addLogLine(_repeatChar('=', 70)); _addLogLine('Enhanced with automatic reconnection'); _addLogLine(_repeatChar('=', 70)); _addLogLine(''); } void _addLogLine(String line) { setState(() { _logLines.add(line); // Keep last 5000 lines if (_logLines.length > 5000) { _logLines.removeAt(0); } }); if (_autoScroll) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); } }); } } void _startStatusTimer() { _statusTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (mounted) { final ble = Provider.of(context, listen: false); if (ble.isConnected) { _updateStatus(ble); } } }); } void _updateStatus(BLEService ble) { final stats = _getStatistics(ble); final elapsed = stats['elapsed'] as double; final packets = stats['packets'] as int; final rate = stats['rate'] as double; final connStatus = ble.isConnected ? '✓' : '✗'; // Remove old status line if exists (lines ending with specific pattern) if (_logLines.isNotEmpty && _logLines.last.startsWith('[STATUS]')) { _logLines.removeLast(); } if (packets > 0) { _addLogLine( '[STATUS] Packets: ${packets.toString().padLeft(5)} | ' 'Rate: ${rate.toStringAsFixed(2).padLeft(5)} pkt/s | ' 'Time: ${elapsed.toStringAsFixed(1).padLeft(7)}s | ' 'Conn: $connStatus', ); } else { _addLogLine('[STATUS] Waiting for data... (${elapsed.toStringAsFixed(1)}s)'); } } Map _getStatistics(BLEService ble) { final now = DateTime.now(); final startTime = ble.streamingDuration != null ? now.subtract(ble.streamingDuration!) : now; final elapsed = now.difference(startTime).inMilliseconds / 1000.0; final packets = ble.packetCount; final rate = elapsed > 0 ? packets / elapsed : 0.0; return { 'elapsed': elapsed, 'packets': packets, 'rate': rate, }; } String _formatTimestamp(DateTime dt) { return '${dt.hour.toString().padLeft(2, '0')}:' '${dt.minute.toString().padLeft(2, '0')}:' '${dt.second.toString().padLeft(2, '0')}'; } void _onDataReceived(DataPacket packet) { final timestamp = _formatTimestamp(packet.timestamp); final hexData = packet.hexString; final packetNum = packet.packetNumber; final bytes = packet.data.length; _addLogLine(''); _addLogLine('[$timestamp] Packet #${packetNum.toString().padLeft(5, '0')}: $hexData ($bytes bytes)'); // Parse data formats (matching Python example) if (packet.data.length >= 4) { try { final valueU32 = packet.value32BitUnsigned; final valueS32 = packet.value32BitSigned; final valueU16 = packet.value16BitUnsigned; final valueS16 = packet.value16BitSigned; // Commented out like in Python example // _addLogLine(' → 32-bit unsigned: ${valueU32.toString().padLeft(10)} | 32-bit signed: ${valueS32.toString().padLeft(10)}'); // _addLogLine(' → 16-bit unsigned: ${valueU16.toString().padLeft(10)} | 16-bit signed: ${valueS16.toString().padLeft(10)}'); if (valueU32 != null && valueU32 > 0) { final floatVal = valueU32 / 100.0; // _addLogLine(' → As float (÷100): ${floatVal.toStringAsFixed(2).padLeft(12)}'); } } catch (e) { _addLogLine(' → Parse error: $e'); } } // Show weight as 'timbang' in KG (matching Python example) final weightKg = packet.weightKg; if (weightKg != null) { _addLogLine(' → timbang : ${weightKg.toStringAsFixed(1)} KG'); } else { // Fallback to showing raw bytes in decimal final byteStr = packet.data.map((b) => b.toString().padLeft(2, '0')).join(' '); _addLogLine(' → timbang : $byteStr'); } // Try to decode as ASCII if printable final asciiStr = packet.asciiString; if (asciiStr != null && asciiStr.isNotEmpty) { _addLogLine(" → ASCII: '$asciiStr'"); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: const Text('SIAB Bluetooth CLI', style: TextStyle(color: Colors.green)), backgroundColor: Colors.grey[900], iconTheme: const IconThemeData(color: Colors.green), actions: [ Consumer( builder: (context, ble, _) { if (ble.isConnected) { return IconButton( icon: const Icon(Icons.delete_outline, color: Colors.green), tooltip: 'Clear Log', onPressed: () { setState(() { _logLines.clear(); _addHeader(); }); ble.clearData(); }, ); } return const SizedBox.shrink(); }, ), IconButton( icon: Icon( _autoScroll ? Icons.vertical_align_bottom : Icons.vertical_align_center, color: Colors.green, ), tooltip: _autoScroll ? 'Disable Auto-scroll' : 'Enable Auto-scroll', onPressed: () { setState(() { _autoScroll = !_autoScroll; }); }, ), ], ), body: Consumer( builder: (context, ble, _) { // Process new data packets if (ble.dataPackets.isNotEmpty) { final newPackets = ble.dataPackets .where((p) => p.packetNumber > _lastProcessedPacket) .toList(); for (final packet in newPackets) { _onDataReceived(packet); _lastProcessedPacket = packet.packetNumber; } } else { _lastProcessedPacket = 0; } return Column( children: [ // Connection status bar (terminal style) Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: Colors.grey[900], child: Row( children: [ Icon( ble.isConnected ? Icons.bluetooth_connected : Icons.bluetooth_disabled, color: ble.isConnected ? Colors.green : Colors.red, size: 20, ), const SizedBox(width: 8), Text( ble.isConnected ? 'Connected: ${ble.deviceName ?? ble.deviceAddress ?? "Unknown"}' : 'Disconnected', style: const TextStyle( color: Colors.green, fontFamily: 'monospace', fontSize: 12, ), ), const Spacer(), if (ble.isScanning || ble.isConnecting) const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.green), ), ), ], ), ), // Terminal output area Expanded( child: Container( color: Colors.black, child: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(16), child: SelectableText( _logLines.join('\n'), style: const TextStyle( color: Colors.green, fontFamily: 'monospace', fontSize: 12, height: 1.4, ), ), ), ), ), // Bottom action bar Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: Colors.grey[900], child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (ble.isConnecting || ble.isScanning) const Text( 'Connecting...', style: TextStyle(color: Colors.yellow, fontFamily: 'monospace'), ) else if (ble.isConnected) ElevatedButton.icon( onPressed: () async { _addLogLine('\n⏹ Disconnecting...'); await ble.disconnect(); ble.clearData(); _addLogLine('✅ Disconnected\n'); }, icon: const Icon(Icons.bluetooth_disabled, size: 18), label: const Text('Disconnect'), style: ElevatedButton.styleFrom( backgroundColor: Colors.red[900], foregroundColor: Colors.white, ), ) else ElevatedButton.icon( onPressed: () async { _addLogLine('\n[1/3] Connecting to SIAB device...'); final success = await ble.autoConnect(); if (success) { _addLogLine('✅ Connected successfully!'); if (ble.deviceAddress != null) { _addLogLine(' Device address: ${ble.deviceAddress}'); } _addLogLine('\n[2/3] Enabling streaming data notifications...'); _addLogLine('✅ Notifications enabled!'); _addLogLine('\n[3/3] Receiving streaming data...'); _addLogLine(_repeatChar('=', 70)); _addLogLine('Waiting for data packets...'); _addLogLine(''); } else { _addLogLine('❌ Failed to connect!'); _addLogLine('\nTroubleshooting:'); _addLogLine(' • Make sure SIAB device is powered on'); _addLogLine(' • Check Bluetooth is enabled'); _addLogLine(' • Verify device name is \'SIAB\''); } }, icon: const Icon(Icons.bluetooth_searching, size: 18), label: const Text('Connect'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green[900], foregroundColor: Colors.white, ), ), ], ), ), ], ); }, ), ); } }