feat: Add display name getter to DeviceInfo and update usage in HomeScreen; refactor contact type handling in MapMarkers

This commit is contained in:
Janez T
2025-10-14 11:18:59 +02:00
parent 373998bc7d
commit ccec842672
8 changed files with 1285 additions and 22 deletions

View File

@@ -114,6 +114,15 @@ class DeviceInfo {
.join(''); .join('');
} }
/// Get display name with "MeshCore-" prefix removed
String? get displayName {
if (deviceName == null) return null;
if (deviceName!.startsWith('MeshCore-')) {
return deviceName!.substring(9); // Remove "MeshCore-" (9 characters)
}
return deviceName;
}
DeviceInfo copyWith({ DeviceInfo copyWith({
String? deviceId, String? deviceId,
String? deviceName, String? deviceName,

View File

@@ -104,6 +104,31 @@ class ConnectionProvider with ChangeNotifier {
onTelemetryReceived?.call(publicKey, lppData); onTelemetryReceived?.call(publicKey, lppData);
}; };
_bleService.onSelfInfoReceived = (selfInfo) {
print('📥 [Provider] Received SelfInfo:');
print(' TX Power: ${selfInfo['txPower']} / ${selfInfo['maxTxPower']} dBm');
print(' Radio: freq=${selfInfo['radioFreq']}, bw=${selfInfo['radioBw']}, sf=${selfInfo['radioSf']}, cr=${selfInfo['radioCr']}');
print(' Position: ${selfInfo['advLat'] / 1000000.0}, ${selfInfo['advLon'] / 1000000.0}');
print(' Self Name: ${selfInfo['selfName']}');
_deviceInfo = _deviceInfo.copyWith(
deviceType: selfInfo['deviceType'] as int?,
txPower: selfInfo['txPower'] as int?,
maxTxPower: selfInfo['maxTxPower'] as int?,
publicKey: selfInfo['publicKey'] as Uint8List?,
advLat: selfInfo['advLat'] as int?,
advLon: selfInfo['advLon'] as int?,
manualAddContacts: selfInfo['manualAddContacts'] as bool?,
radioFreq: selfInfo['radioFreq'] as int?,
radioBw: selfInfo['radioBw'] as int?,
radioSf: selfInfo['radioSf'] as int?,
radioCr: selfInfo['radioCr'] as int?,
selfName: selfInfo['selfName'] as String?,
);
notifyListeners();
print('✅ [Provider] Device info updated with SelfInfo');
};
// Activity indicators // Activity indicators
_bleService.onRxActivity = () { _bleService.onRxActivity = () {
_rxActivity = true; _rxActivity = true;
@@ -299,6 +324,103 @@ class ConnectionProvider with ChangeNotifier {
} }
} }
/// Set advertised name
Future<void> setAdvertName(String name) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.setAdvertName(name);
} catch (e) {
_error = 'Failed to set name: $e';
notifyListeners();
}
}
/// Set advertised position
Future<void> setAdvertLatLon({
required double latitude,
required double longitude,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.setAdvertLatLon(
latitude: latitude,
longitude: longitude,
);
} catch (e) {
_error = 'Failed to set position: $e';
notifyListeners();
}
}
/// Set radio parameters
Future<void> setRadioParams({
required int frequency,
required int bandwidth,
required int spreadingFactor,
required int codingRate,
}) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.setRadioParams(
frequency: frequency,
bandwidth: bandwidth,
spreadingFactor: spreadingFactor,
codingRate: codingRate,
);
} catch (e) {
_error = 'Failed to set radio params: $e';
notifyListeners();
}
}
/// Set transmit power
Future<void> setTxPower(int powerDbm) async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
await _bleService.setTxPower(powerDbm);
} catch (e) {
_error = 'Failed to set TX power: $e';
notifyListeners();
}
}
/// Request fresh device info (triggers SelfInfo response)
Future<void> refreshDeviceInfo() async {
if (!_bleService.isConnected) {
_error = 'Not connected to device';
notifyListeners();
return;
}
try {
// The device query command triggers a SelfInfo response
await _bleService.refreshDeviceInfo();
} catch (e) {
_error = 'Failed to refresh device info: $e';
notifyListeners();
}
}
/// Clear error message /// Clear error message
void clearError() { void clearError() {
_error = null; _error = null;

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,13 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/connection_provider.dart'; import '../providers/connection_provider.dart';
import '../providers/app_provider.dart'; import '../providers/app_provider.dart';
import '../models/device_info.dart' as models;
import '../services/tile_cache_service.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
import 'messages_tab.dart'; import 'messages_tab.dart';
import 'contacts_tab.dart'; import 'contacts_tab.dart';
import 'map_tab.dart'; import 'map_tab.dart';
import 'map_management_screen.dart'; import 'map_management_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
import 'device_config_screen.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged; final Function(AppThemeMode) onThemeChanged;
@@ -352,7 +351,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
Text( Text(
isConnected isConnected
? deviceInfo.deviceName ?? 'Connected' ? deviceInfo.displayName ?? 'Connected'
: 'Disconnected', : 'Disconnected',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
@@ -420,22 +419,42 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
OutlinedButton( // Settings button
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DeviceConfigScreen(),
),
);
},
icon: const Icon(Icons.settings),
iconSize: 20,
tooltip: 'Device Settings',
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
),
const SizedBox(width: 8),
// Disconnect button (prominent, icon only)
FilledButton(
onPressed: () async { onPressed: () async {
await provider.disconnect(); await provider.disconnect();
if (context.mounted) { if (context.mounted) {
context.read<AppProvider>().clearAllData(); context.read<AppProvider>().clearAllData();
} }
}, },
style: OutlinedButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white, foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white), padding: const EdgeInsets.all(10),
shape: RoundedRectangleBorder( minimumSize: const Size(40, 40),
borderRadius: BorderRadius.circular(20), shape: const CircleBorder(),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
), ),
child: const Text('Disconnect', style: TextStyle(fontSize: 13)), child: const Icon(Icons.power_settings_new, size: 20),
), ),
], ],
), ),
@@ -467,7 +486,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
Expanded( Expanded(
child: Text( child: Text(
isConnected isConnected
? deviceInfo.deviceName ?? 'Connected' ? deviceInfo.displayName ?? 'Connected'
: 'Not Connected', : 'Not Connected',
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),

View File

@@ -687,7 +687,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
super.build(context); // Required for AutomaticKeepAliveClientMixin super.build(context); // Required for AutomaticKeepAliveClientMixin
return Consumer2<ContactsProvider, MessagesProvider>( return Consumer2<ContactsProvider, MessagesProvider>(
builder: (context, contactsProvider, messagesProvider, child) { builder: (context, contactsProvider, messagesProvider, child) {
final contactsWithLocation = contactsProvider.chatContactsWithLocation; final contactsWithLocation = contactsProvider.contactsWithLocation;
final sarMarkers = messagesProvider.sarMarkers; final sarMarkers = messagesProvider.sarMarkers;
final center = _calculateCenter(contactsWithLocation, sarMarkers); final center = _calculateCenter(contactsWithLocation, sarMarkers);
@@ -729,7 +729,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
onContactTap: (contact) { onContactTap: (contact) {
_showDetailedCompassWithContact( _showDetailedCompassWithContact(
context, context,
contactsProvider.chatContactsWithLocation, contactsProvider.contactsWithLocation,
messagesProvider.sarMarkers, messagesProvider.sarMarkers,
contact, contact,
); );
@@ -742,7 +742,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
onSarMarkerTap: (marker) { onSarMarkerTap: (marker) {
_showDetailedCompassWithSarMarker( _showDetailedCompassWithSarMarker(
context, context,
contactsProvider.chatContactsWithLocation, contactsProvider.contactsWithLocation,
messagesProvider.sarMarkers, messagesProvider.sarMarkers,
marker, marker,
); );
@@ -807,7 +807,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
child: GestureDetector( child: GestureDetector(
onTap: () => _showDetailedCompass( onTap: () => _showDetailedCompass(
context, context,
contactsProvider.chatContactsWithLocation, contactsProvider.contactsWithLocation,
messagesProvider.sarMarkers, messagesProvider.sarMarkers,
), ),
child: _CompassWidget( child: _CompassWidget(

View File

@@ -14,6 +14,7 @@ typedef OnContactCallback = void Function(Contact contact);
typedef OnContactsCompleteCallback = void Function(List<Contact> contacts); typedef OnContactsCompleteCallback = void Function(List<Contact> contacts);
typedef OnMessageCallback = void Function(Message message); typedef OnMessageCallback = void Function(Message message);
typedef OnTelemetryCallback = void Function(Uint8List publicKey, Uint8List lppData); typedef OnTelemetryCallback = void Function(Uint8List publicKey, Uint8List lppData);
typedef OnSelfInfoCallback = void Function(Map<String, dynamic> selfInfo);
typedef OnErrorCallback = void Function(String error); typedef OnErrorCallback = void Function(String error);
typedef OnConnectionStateCallback = void Function(bool isConnected); typedef OnConnectionStateCallback = void Function(bool isConnected);
@@ -30,6 +31,7 @@ class MeshCoreBleService {
OnContactsCompleteCallback? onContactsComplete; OnContactsCompleteCallback? onContactsComplete;
OnMessageCallback? onMessageReceived; OnMessageCallback? onMessageReceived;
OnTelemetryCallback? onTelemetryReceived; OnTelemetryCallback? onTelemetryReceived;
OnSelfInfoCallback? onSelfInfoReceived;
OnErrorCallback? onError; OnErrorCallback? onError;
// Internal state // Internal state
@@ -636,11 +638,29 @@ class MeshCoreBleService {
print(' Position: ${advLat / 1000000.0}, ${advLon / 1000000.0}'); print(' Position: ${advLat / 1000000.0}, ${advLon / 1000000.0}');
print(' Radio: freq=$radioFreq, bw=$radioBw, sf=$radioSf, cr=$radioCr'); print(' Radio: freq=$radioFreq, bw=$radioBw, sf=$radioSf, cr=$radioCr');
String? selfName;
if (reader.hasRemaining) { if (reader.hasRemaining) {
final selfName = String.fromCharCodes(reader.readRemainingBytes().takeWhile((b) => b != 0)); selfName = String.fromCharCodes(reader.readRemainingBytes().takeWhile((b) => b != 0));
print(' Self name: $selfName'); print(' Self name: $selfName');
} }
// Call callback with parsed data
onSelfInfoReceived?.call({
'protocolVersion': protocolVersion,
'deviceType': deviceType,
'txPower': txPower,
'maxTxPower': maxTxPower,
'publicKey': publicKey,
'advLat': advLat,
'advLon': advLon,
'manualAddContacts': manualAddContacts == 1,
'radioFreq': radioFreq,
'radioBw': radioBw,
'radioSf': radioSf,
'radioCr': radioCr,
'selfName': selfName,
});
print(' ✅ [SelfInfo] Parsed successfully'); print(' ✅ [SelfInfo] Parsed successfully');
} catch (e) { } catch (e) {
print(' ❌ [SelfInfo] Parsing error: $e'); print(' ❌ [SelfInfo] Parsing error: $e');
@@ -718,6 +738,11 @@ class MeshCoreBleService {
await _sendAppStart(); await _sendAppStart();
} }
/// Refresh device info (public method)
Future<void> refreshDeviceInfo() async {
await _sendDeviceQuery();
}
/// Get contacts from device /// Get contacts from device
Future<void> getContacts() async { Future<void> getContacts() async {
final writer = BufferWriter(); final writer = BufferWriter();
@@ -793,6 +818,50 @@ class MeshCoreBleService {
await _writeData(writer.toBytes()); await _writeData(writer.toBytes());
} }
/// Set advertised name
Future<void> setAdvertName(String name) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSetAdvertName);
writer.writeString(name);
await _writeData(writer.toBytes());
}
/// Set advertised latitude and longitude
Future<void> setAdvertLatLon({
required double latitude,
required double longitude,
}) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSetAdvertLatLon);
writer.writeInt32LE((latitude * 1000000).round());
writer.writeInt32LE((longitude * 1000000).round());
await _writeData(writer.toBytes());
}
/// Set radio parameters
Future<void> setRadioParams({
required int frequency, // Hz
required int bandwidth, // 0-9 (see bandwidth options)
required int spreadingFactor, // 7-12
required int codingRate, // 5-8
}) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSetRadioParams);
writer.writeUInt32LE(frequency);
writer.writeUInt16LE(bandwidth);
writer.writeByte(spreadingFactor);
writer.writeByte(codingRate);
await _writeData(writer.toBytes());
}
/// Set transmit power
Future<void> setTxPower(int powerDbm) async {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSetTxPower);
writer.writeByte(powerDbm);
await _writeData(writer.toBytes());
}
/// Reset packet counters /// Reset packet counters
void resetCounters() { void resetCounters() {
_rxPacketCount = 0; _rxPacketCount = 0;

View File

@@ -72,8 +72,8 @@ class SampleDataGenerator {
outPath: Uint8List(32), outPath: Uint8List(32),
advName: teamNames[i], advName: teamNames[i],
lastAdvert: now.millisecondsSinceEpoch ~/ 1000, lastAdvert: now.millisecondsSinceEpoch ~/ 1000,
advLat: (lat * 1e7).toInt(), advLat: (lat * 1e6).toInt(),
advLon: (lon * 1e7).toInt(), advLon: (lon * 1e6).toInt(),
lastMod: now.millisecondsSinceEpoch ~/ 1000, lastMod: now.millisecondsSinceEpoch ~/ 1000,
telemetry: telemetry, telemetry: telemetry,
); );

View File

@@ -52,7 +52,7 @@ class MapMarkers {
// Marker icon // Marker icon
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary, color: _getContactTypeColor(contact, context),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 2),
boxShadow: [ boxShadow: [
@@ -64,8 +64,8 @@ class MapMarkers {
], ],
), ),
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: const Icon( child: Icon(
Icons.person, _getContactTypeIcon(contact),
color: Colors.white, color: Colors.white,
size: 18, size: 18,
), ),
@@ -294,6 +294,32 @@ class MapMarkers {
return Colors.grey; return Colors.grey;
} }
} }
static Color _getContactTypeColor(Contact contact, BuildContext context) {
switch (contact.type) {
case ContactType.chat:
return Theme.of(context).colorScheme.primary; // Blue for team members
case ContactType.repeater:
return Colors.deepPurple; // Purple for repeaters
case ContactType.room:
return Colors.teal; // Teal for rooms/channels
case ContactType.none:
return Colors.grey;
}
}
static IconData _getContactTypeIcon(Contact contact) {
switch (contact.type) {
case ContactType.chat:
return Icons.person; // Person for team members
case ContactType.repeater:
return Icons.router; // Router icon for repeaters
case ContactType.room:
return Icons.forum; // Forum/chat icon for rooms
case ContactType.none:
return Icons.help_outline;
}
}
} }
class _InfoRow extends StatelessWidget { class _InfoRow extends StatelessWidget {