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('');
}
/// 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({
String? deviceId,
String? deviceName,

View File

@@ -104,6 +104,31 @@ class ConnectionProvider with ChangeNotifier {
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
_bleService.onRxActivity = () {
_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
void clearError() {
_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 '../providers/connection_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 'messages_tab.dart';
import 'contacts_tab.dart';
import 'map_tab.dart';
import 'map_management_screen.dart';
import 'settings_screen.dart';
import 'device_config_screen.dart';
class HomeScreen extends StatefulWidget {
final Function(AppThemeMode) onThemeChanged;
@@ -352,7 +351,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
),
Text(
isConnected
? deviceInfo.deviceName ?? 'Connected'
? deviceInfo.displayName ?? 'Connected'
: 'Disconnected',
style: TextStyle(
fontSize: 14,
@@ -420,22 +419,42 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
),
),
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 {
await provider.disconnect();
if (context.mounted) {
context.read<AppProvider>().clearAllData();
}
},
style: OutlinedButton.styleFrom(
style: FilledButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.all(10),
minimumSize: const Size(40, 40),
shape: const CircleBorder(),
),
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(
child: Text(
isConnected
? deviceInfo.deviceName ?? 'Connected'
? deviceInfo.displayName ?? 'Connected'
: 'Not Connected',
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
return Consumer2<ContactsProvider, MessagesProvider>(
builder: (context, contactsProvider, messagesProvider, child) {
final contactsWithLocation = contactsProvider.chatContactsWithLocation;
final contactsWithLocation = contactsProvider.contactsWithLocation;
final sarMarkers = messagesProvider.sarMarkers;
final center = _calculateCenter(contactsWithLocation, sarMarkers);
@@ -729,7 +729,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
onContactTap: (contact) {
_showDetailedCompassWithContact(
context,
contactsProvider.chatContactsWithLocation,
contactsProvider.contactsWithLocation,
messagesProvider.sarMarkers,
contact,
);
@@ -742,7 +742,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
onSarMarkerTap: (marker) {
_showDetailedCompassWithSarMarker(
context,
contactsProvider.chatContactsWithLocation,
contactsProvider.contactsWithLocation,
messagesProvider.sarMarkers,
marker,
);
@@ -807,7 +807,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
child: GestureDetector(
onTap: () => _showDetailedCompass(
context,
contactsProvider.chatContactsWithLocation,
contactsProvider.contactsWithLocation,
messagesProvider.sarMarkers,
),
child: _CompassWidget(

View File

@@ -14,6 +14,7 @@ typedef OnContactCallback = void Function(Contact contact);
typedef OnContactsCompleteCallback = void Function(List<Contact> contacts);
typedef OnMessageCallback = void Function(Message message);
typedef OnTelemetryCallback = void Function(Uint8List publicKey, Uint8List lppData);
typedef OnSelfInfoCallback = void Function(Map<String, dynamic> selfInfo);
typedef OnErrorCallback = void Function(String error);
typedef OnConnectionStateCallback = void Function(bool isConnected);
@@ -30,6 +31,7 @@ class MeshCoreBleService {
OnContactsCompleteCallback? onContactsComplete;
OnMessageCallback? onMessageReceived;
OnTelemetryCallback? onTelemetryReceived;
OnSelfInfoCallback? onSelfInfoReceived;
OnErrorCallback? onError;
// Internal state
@@ -636,11 +638,29 @@ class MeshCoreBleService {
print(' Position: ${advLat / 1000000.0}, ${advLon / 1000000.0}');
print(' Radio: freq=$radioFreq, bw=$radioBw, sf=$radioSf, cr=$radioCr');
String? selfName;
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');
}
// 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');
} catch (e) {
print(' ❌ [SelfInfo] Parsing error: $e');
@@ -718,6 +738,11 @@ class MeshCoreBleService {
await _sendAppStart();
}
/// Refresh device info (public method)
Future<void> refreshDeviceInfo() async {
await _sendDeviceQuery();
}
/// Get contacts from device
Future<void> getContacts() async {
final writer = BufferWriter();
@@ -793,6 +818,50 @@ class MeshCoreBleService {
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
void resetCounters() {
_rxPacketCount = 0;

View File

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

View File

@@ -52,7 +52,7 @@ class MapMarkers {
// Marker icon
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
color: _getContactTypeColor(contact, context),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
@@ -64,8 +64,8 @@ class MapMarkers {
],
),
padding: const EdgeInsets.all(6),
child: const Icon(
Icons.person,
child: Icon(
_getContactTypeIcon(contact),
color: Colors.white,
size: 18,
),
@@ -294,6 +294,32 @@ class MapMarkers {
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 {