mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add display name getter to DeviceInfo and update usage in HomeScreen; refactor contact type handling in MapMarkers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
1018
lib/screens/device_config_screen.dart
Normal file
1018
lib/screens/device_config_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user