diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index 3b95e52..dfdf3bc 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -11,8 +11,8 @@ import 'package:universal_ble_example/peripheral_details/widgets/result_header_b import 'package:universal_ble_example/peripheral_details/widgets/result_widget.dart'; import 'package:universal_ble_example/peripheral_details/widgets/services_list_widget.dart'; import 'package:universal_ble_example/peripheral_details/widgets/services_side_widget.dart'; +import 'package:universal_ble_example/data/company_identifier_service.dart'; import 'package:universal_ble_example/data/scan_controller.dart'; -import 'package:universal_ble_example/widgets/company_info_widget.dart'; import 'package:universal_ble_example/widgets/responsive_view.dart'; import 'package:universal_ble_example/widgets/scan_controller_scope.dart'; @@ -622,7 +622,6 @@ class _PeripheralDetailPageState extends State { child: SingleChildScrollView( child: Column( children: [ - // Device info with manufacturer data and advertised services _buildDeviceInfo(), // Received advertisements list @@ -1148,161 +1147,6 @@ class _PeripheralDetailPageState extends State { bleDevice.name ?? "Unknown", Icons.label, ), - // Manufacturer data - if (bleDevice.manufacturerDataList.isNotEmpty) ...[ - const SizedBox(height: 16), - Row( - children: [ - Icon( - Icons.memory, - size: 18, - color: colorScheme.secondary, - ), - const SizedBox(width: 8), - Text( - 'Manufacturer Data', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: colorScheme.onSurface, - ), - ), - ], - ), - const SizedBox(height: 8), - ...bleDevice.manufacturerDataList.map( - (data) => Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - ), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - 'Company ID: ', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.onSecondaryContainer, - ), - ), - Expanded( - child: SelectableText( - data.companyIdRadix16, - style: TextStyle( - fontSize: 12, - fontFamily: 'monospace', - color: colorScheme.onSecondaryContainer, - ), - ), - ), - ], - ), - CompanyInfoWidget( - companyId: data.companyId, - colorScheme: colorScheme, - labelStyle: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.onSecondaryContainer, - ), - nameStyle: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: colorScheme.onSecondaryContainer, - ), - ), - if (data.payloadRadix16.isNotEmpty) ...[ - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Payload: ', - style: TextStyle( - fontSize: 11, - color: colorScheme.onSecondaryContainer - .withValues(alpha: 0.8), - ), - ), - Expanded( - child: SelectableText( - data.payloadRadix16, - style: TextStyle( - fontSize: 11, - fontFamily: 'monospace', - color: colorScheme.onSecondaryContainer - .withValues(alpha: 0.8), - ), - ), - ), - ], - ), - ], - ], - ), - ), - ), - ), - ], - // Advertised services - if (bleDevice.services.isNotEmpty) ...[ - const SizedBox(height: 16), - Row( - children: [ - Icon( - Icons.list, - size: 18, - color: colorScheme.tertiary, - ), - const SizedBox(width: 8), - Text( - 'Advertised Services', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: colorScheme.onSurface, - ), - ), - ], - ), - const SizedBox(height: 8), - Wrap( - spacing: 6, - runSpacing: 6, - children: bleDevice.services - .map( - (service) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: SelectableText( - service, - style: TextStyle( - fontSize: 11, - fontFamily: 'monospace', - color: colorScheme.onTertiaryContainer, - fontWeight: FontWeight.w500, - ), - ), - ), - ) - .toList(), - ), - ], ], ), ), @@ -1316,15 +1160,63 @@ class _PeripheralDetailPageState extends State { '${dt.second.toString().padLeft(2, '0')}'; } - static String _advertisementPayload(BleDevice device) { - if (device.manufacturerDataList.isNotEmpty) { - return device.manufacturerDataList.first.payloadRadix16; - } - if (device.serviceData.isNotEmpty) { - final entry = device.serviceData.entries.first; - return '0x${entry.value.map((b) => b.toRadixString(16).padLeft(2, '0')).join('').toUpperCase()}'; - } - return '—'; + static String _formatCompanyIdentifiers(BleDevice device) { + if (device.manufacturerDataList.isEmpty) return '—'; + final companyService = CompanyIdentifierService.instance; + return device.manufacturerDataList + .map((d) { + final name = companyService.getCompanyName(d.companyId); + return name != null ? '${d.companyIdRadix16} ($name)' : d.companyIdRadix16; + }) + .join(', '); + } + + static String _formatManufacturerData(BleDevice device) { + if (device.manufacturerDataList.isEmpty) return '—'; + return device.manufacturerDataList + .map((d) => '${d.companyIdRadix16}: ${d.payloadRadix16}') + .join('; '); + } + + static String _formatServiceData(BleDevice device) { + if (device.serviceData.isEmpty) return '—'; + return device.serviceData.entries + .map((e) => + '${e.key}: 0x${e.value.map((b) => b.toRadixString(16).padLeft(2, '0')).join('').toUpperCase()}') + .join('; '); + } + + static String _formatAdvertisedServices(BleDevice device) { + if (device.services.isEmpty) return '—'; + return device.services.join(', '); + } + + Widget _buildAdRow( + String label, + String value, + IconData icon, + TextStyle labelStyle, + TextStyle valueStyle, + ColorScheme colorScheme, + ) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 12, color: colorScheme.onSurface.withValues(alpha: 0.6)), + const SizedBox(width: 4), + SizedBox( + width: 98, + child: Text(label, style: labelStyle), + ), + Expanded( + child: SelectableText( + value, + style: valueStyle, + maxLines: 2, + ), + ), + ], + ); } Future _copyReceivedAdvertisements() async { @@ -1332,8 +1224,12 @@ class _PeripheralDetailPageState extends State { final lines = _receivedAdvertisements.reversed.map((ad) { final ts = ad.timestampDateTime; final timeStr = ts != null ? _formatAdTime(ts) : '—'; - final payload = _advertisementPayload(ad); - return '$timeStr RSSI: ${ad.rssi ?? "—"} $payload'; + final rssiStr = ad.rssi?.toString() ?? '—'; + final companyIdStr = _formatCompanyIdentifiers(ad); + final mfrStr = _formatManufacturerData(ad); + final svcStr = _formatServiceData(ad); + final advSvcStr = _formatAdvertisedServices(ad); + return 'Time: $timeStr RSSI: $rssiStr Company ID: $companyIdStr Manufacturer Data: $mfrStr Service Data: $svcStr Advertised Services: $advSvcStr'; }).toList(); await Clipboard.setData(ClipboardData(text: lines.join('\n'))); if (mounted) { @@ -1341,6 +1237,22 @@ class _PeripheralDetailPageState extends State { } } + Future _copyAdvertisement(BleDevice ad) async { + final ts = ad.timestampDateTime; + final timeStr = ts != null ? _formatAdTime(ts) : '—'; + final rssiStr = ad.rssi?.toString() ?? '—'; + final companyIdStr = _formatCompanyIdentifiers(ad); + final mfrStr = _formatManufacturerData(ad); + final svcStr = _formatServiceData(ad); + final advSvcStr = _formatAdvertisedServices(ad); + final line = + 'Time: $timeStr RSSI: $rssiStr Company ID: $companyIdStr Manufacturer Data: $mfrStr Service Data: $svcStr Advertised Services: $advSvcStr'; + await Clipboard.setData(ClipboardData(text: line)); + if (mounted) { + _showSnackBar('Advertisement copied to clipboard'); + } + } + Widget _buildReceivedAdvertisements() { final colorScheme = Theme.of(context).colorScheme; return Padding( @@ -1365,13 +1277,13 @@ class _PeripheralDetailPageState extends State { }); }, tilePadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + horizontal: 12, + vertical: 6, ), childrenPadding: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, + left: 12, + right: 12, + bottom: 10, ), title: ResultHeaderBar( icon: Icons.ad_units_outlined, @@ -1399,21 +1311,30 @@ class _PeripheralDetailPageState extends State { children: [ _receivedAdvertisements.isEmpty ? Padding( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: const EdgeInsets.symmetric(vertical: 10), child: Text( 'No advertisements yet', style: TextStyle( - fontSize: 13, + fontSize: 12, color: colorScheme.onSurface.withValues(alpha: 0.6), ), ), ) : ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200), + constraints: const BoxConstraints(maxHeight: 240), child: ListView.separated( shrinkWrap: true, itemCount: _receivedAdvertisements.length, - separatorBuilder: (_, __) => const SizedBox(height: 6), + separatorBuilder: (context, _) => Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Divider( + height: 1, + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.2), + ), + ), itemBuilder: (context, index) { final ad = _receivedAdvertisements[ _receivedAdvertisements.length - 1 - index]; @@ -1421,56 +1342,107 @@ class _PeripheralDetailPageState extends State { final timeStr = ts != null ? _formatAdTime(ts) : '—'; - final payload = _advertisementPayload(ad); + final companyIdStr = + _formatCompanyIdentifiers(ad); + final mfrStr = _formatManufacturerData(ad); + final svcStr = _formatServiceData(ad); + final advSvcStr = _formatAdvertisedServices(ad); + final labelStyle = TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface + .withValues(alpha: 0.7), + ); + final valueStyle = TextStyle( + fontSize: 10, + fontFamily: 'monospace', + color: colorScheme.onSurface + .withValues(alpha: 0.85), + ); return Padding( padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 8, + vertical: 3, + horizontal: 4, ), - child: Column( + child: Row( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ - Row( - children: [ - Icon( - Icons.schedule, - size: 14, - color: colorScheme.onSurface - .withValues(alpha: 0.6), - ), - const SizedBox(width: 8), - Text( - timeStr, - style: TextStyle( - fontSize: 12, - fontFamily: 'monospace', - color: colorScheme.onSurface - .withValues(alpha: 0.8), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildAdRow( + 'Time', + timeStr, + Icons.schedule, + labelStyle, + valueStyle, + colorScheme, ), - ), - const SizedBox(width: 12), - Text( - 'RSSI: ${ad.rssi ?? "—"}', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurface - .withValues(alpha: 0.8), + const SizedBox(height: 2), + _buildAdRow( + 'RSSI', + ad.rssi?.toString() ?? '—', + Icons.signal_cellular_alt, + labelStyle, + valueStyle, + colorScheme, ), - ), - ], - ), - const SizedBox(height: 4), - Text( - payload, - style: TextStyle( - fontSize: 11, - fontFamily: 'monospace', - color: colorScheme.onSurface - .withValues(alpha: 0.7), + const SizedBox(height: 2), + _buildAdRow( + 'Company ID', + companyIdStr, + Icons.business, + labelStyle, + valueStyle, + colorScheme, + ), + const SizedBox(height: 2), + _buildAdRow( + 'Adv. Services', + advSvcStr, + Icons.apps, + labelStyle, + valueStyle, + colorScheme, + ), + const SizedBox(height: 2), + _buildAdRow( + 'Mfr Data', + mfrStr, + Icons.memory, + labelStyle, + valueStyle, + colorScheme, + ), + const SizedBox(height: 2), + _buildAdRow( + 'Service Data', + svcStr, + Icons.list, + labelStyle, + valueStyle, + colorScheme, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.copy), + iconSize: 18, + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + tooltip: 'Copy advertisement', + onPressed: () => _copyAdvertisement(ad), + style: IconButton.styleFrom( + foregroundColor: colorScheme.onSurface + .withValues(alpha: 0.6), ), - overflow: TextOverflow.ellipsis, - maxLines: 2, ), ], ),