Compare commits

...

4 Commits

Author SHA1 Message Date
Janez T
c299349b6a chore: Update changelog #0 2026-03-22 16:02:17 +01:00
Janez T
ab05f13de9 fix: Remove contact trails from map #123 2026-03-22 15:48:55 +01:00
Janez T
0e61dc5bf1 fix: Revert Darwin BLE override 2026-03-22 13:57:14 +01:00
Janez T
d45924d7b4 fix: Vendor Darwin BLE override 2026-03-22 13:56:23 +01:00
7 changed files with 45 additions and 276 deletions

View File

@@ -1,5 +1,17 @@
# Changelog
## 2026-03-15 to 2026-03-22
- Device connectivity and routing: added Android USB serial companion support, serial compass support, GPS module controls, per-contact path size selection, and favourites support via firmware flags.
- Location and map intelligence: added RSSI-based repeater trilateration and contact location estimates, isolated MET history, matched the official advert import/discovery flow, and refreshed map presentation by removing contact trails.
- Telemetry and device info: surfaced self telemetry and sensor defaults, opened device info from signal and battery taps, improved signal chip behavior, fixed duplicate telemetry delivery, and restored telemetry refresh behavior.
- Contacts and messaging: added meshcore:// contact sharing and repeater neighbour views, supported MeshCore message links, fixed DM retry behavior, corrected room poster naming, and compacted the recipient selector.
- UI polish and platform upkeep: improved device settings layout, tightened sensor and contact presentation, expanded localization coverage, and refreshed platform dependencies and build numbers.
## Key PRs
- [#26](https://github.com/dz0ny/meshcore-sar/pull/26) Merge pull request #26 from `dz0ny/feat/sensor-telemetry-preview`
## 2026-03-08 to 2026-03-14
- Discovery and contacts: finished the discovery flow, removed artificial UI delays, added profile storage and sync, and allowed contact name overrides.

View File

@@ -263,7 +263,6 @@ class MapWorkspaceProfileSection {
final bool? showKrasFireZonesOverlay;
final bool? showPlaceNamesOverlay;
final bool? showMunicipalityBordersOverlay;
final bool? showAllContactTrails;
final bool? hideRepeatersOnMap;
const MapWorkspaceProfileSection({
@@ -284,7 +283,6 @@ class MapWorkspaceProfileSection {
this.showKrasFireZonesOverlay,
this.showPlaceNamesOverlay,
this.showMunicipalityBordersOverlay,
this.showAllContactTrails,
this.hideRepeatersOnMap,
});
@@ -306,7 +304,6 @@ class MapWorkspaceProfileSection {
showKrasFireZonesOverlay == null &&
showPlaceNamesOverlay == null &&
showMunicipalityBordersOverlay == null &&
showAllContactTrails == null &&
hideRepeatersOnMap == null;
Map<String, dynamic> toJson() => {
@@ -327,7 +324,6 @@ class MapWorkspaceProfileSection {
'showKrasFireZonesOverlay': showKrasFireZonesOverlay,
'showPlaceNamesOverlay': showPlaceNamesOverlay,
'showMunicipalityBordersOverlay': showMunicipalityBordersOverlay,
'showAllContactTrails': showAllContactTrails,
'hideRepeatersOnMap': hideRepeatersOnMap,
};
@@ -356,7 +352,6 @@ class MapWorkspaceProfileSection {
showPlaceNamesOverlay: json['showPlaceNamesOverlay'] as bool?,
showMunicipalityBordersOverlay:
json['showMunicipalityBordersOverlay'] as bool?,
showAllContactTrails: json['showAllContactTrails'] as bool?,
hideRepeatersOnMap: json['hideRepeatersOnMap'] as bool?,
);
}

View File

@@ -37,8 +37,6 @@ class MapProvider with ChangeNotifier {
MapCoordinateSpace _targetCoordinateSpace = MapCoordinateSpace.geo;
String? _targetMapId;
final Set<String> _visibleContactPaths = {};
LocationTrail? _currentTrail;
bool _isTrailVisible = true;
final List<LocationTrail> _trailHistory = [];
@@ -55,7 +53,6 @@ class MapProvider with ChangeNotifier {
bool _showPlaceNamesOverlay = false;
bool _showMunicipalityBordersOverlay = false;
bool _showAllContactTrails = true;
bool _hideRepeatersOnMap = false;
LocationTrail? _importedTrail;
@@ -72,8 +69,6 @@ class MapProvider with ChangeNotifier {
bool get shouldAnimate => _shouldAnimate;
MapCoordinateSpace get targetCoordinateSpace => _targetCoordinateSpace;
String? get targetMapId => _targetMapId;
Set<String> get visibleContactPaths => Set.unmodifiable(_visibleContactPaths);
LocationTrail? get currentTrail => _currentTrail;
bool get isTrailVisible => _isTrailVisible;
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
@@ -91,7 +86,6 @@ class MapProvider with ChangeNotifier {
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
bool get showAllContactTrails => _showAllContactTrails;
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
LocationTrail? get importedTrail => _importedTrail;
@@ -365,30 +359,6 @@ class MapProvider with ChangeNotifier {
notifyListeners();
}
void toggleContactPath(String publicKeyHex) {
if (_visibleContactPaths.contains(publicKeyHex)) {
_visibleContactPaths.remove(publicKeyHex);
} else {
_visibleContactPaths.add(publicKeyHex);
}
notifyListeners();
}
bool isContactPathVisible(String publicKeyHex) {
return _visibleContactPaths.contains(publicKeyHex);
}
void hideAllPaths() {
_visibleContactPaths.clear();
notifyListeners();
}
void showOnlyPath(String publicKeyHex) {
_visibleContactPaths.clear();
_visibleContactPaths.add(publicKeyHex);
notifyListeners();
}
void startTrail() {
if (_currentTrail != null && _currentTrail!.isActive) {
endTrail();
@@ -556,12 +526,17 @@ class MapProvider with ChangeNotifier {
Future<void> _loadInitialState() async {
await Future.wait([
loadOverlayState(),
loadTrailSettings(),
loadRepeaterVisibilitySettings(),
_loadCustomMapState(),
_removeDeprecatedSettings(),
]);
}
Future<void> _removeDeprecatedSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_scopedKey('map_show_all_contact_trails'));
}
Future<void> _saveOverlayState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(
@@ -610,27 +585,6 @@ class MapProvider with ChangeNotifier {
);
}
Future<void> toggleAllContactTrails() async {
_showAllContactTrails = !_showAllContactTrails;
notifyListeners();
await _saveTrailSettings();
}
Future<void> loadTrailSettings() async {
final prefs = await SharedPreferences.getInstance();
_showAllContactTrails =
prefs.getBool(_scopedKey('map_show_all_contact_trails')) ?? true;
notifyListeners();
}
Future<void> _saveTrailSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(
_scopedKey('map_show_all_contact_trails'),
_showAllContactTrails,
);
}
Future<void> setHideRepeatersOnMap(bool hide) async {
if (_hideRepeatersOnMap == hide) return;
_hideRepeatersOnMap = hide;
@@ -755,7 +709,6 @@ class MapProvider with ChangeNotifier {
'showKrasFireZonesOverlay': _showKrasFireZonesOverlay,
'showPlaceNamesOverlay': _showPlaceNamesOverlay,
'showMunicipalityBordersOverlay': _showMunicipalityBordersOverlay,
'showAllContactTrails': _showAllContactTrails,
'hideRepeatersOnMap': _hideRepeatersOnMap,
};
}
@@ -792,7 +745,6 @@ class MapProvider with ChangeNotifier {
_showPlaceNamesOverlay = json['showPlaceNamesOverlay'] as bool? ?? false;
_showMunicipalityBordersOverlay =
json['showMunicipalityBordersOverlay'] as bool? ?? false;
_showAllContactTrails = json['showAllContactTrails'] as bool? ?? true;
_hideRepeatersOnMap = json['hideRepeatersOnMap'] as bool? ?? false;
notifyListeners();
}

View File

@@ -27,7 +27,6 @@ import '../services/background_location_service.dart';
import '../services/location_tracking_service.dart';
import '../services/map_marker_service.dart';
import '../services/message_destination_preferences.dart';
import '../services/trail_color_service.dart';
import '../services/profiles_feature_service.dart';
import '../widgets/map_debug_info.dart';
import '../widgets/map/compass_widget.dart';
@@ -873,7 +872,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (!hasCustomMap)
ListTile(
leading: Icon(Icons.add_photo_alternate),
title: Text(AppLocalizations.of(context)!.loadFromGallery),
title: Text(
AppLocalizations.of(context)!.loadFromGallery,
),
subtitle: const Text(
'Use a cave map image instead of GPS tiles',
),
@@ -905,7 +906,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
),
ListTile(
leading: Icon(Icons.swap_horizontal_circle),
title: Text(AppLocalizations.of(context)!.replaceImage),
title: Text(
AppLocalizations.of(context)!.replaceImage,
),
subtitle: const Text(
'Pick a different map from the gallery',
),
@@ -941,7 +944,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (customMapConfig.isCalibrated)
ListTile(
leading: Icon(Icons.clear),
title: Text(AppLocalizations.of(context)!.clearScale),
title: Text(
AppLocalizations.of(context)!.clearScale,
),
onTap: () async {
await mapProvider.clearCustomMapCalibration();
if (!context.mounted) return;
@@ -1277,9 +1282,11 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
_stopCustomMapCalibration();
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.customMapScaleSaved)));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.customMapScaleSaved),
),
);
}
}
@@ -1419,7 +1426,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
contentPadding: EdgeInsets.zero,
leading: Icon(Icons.chat_bubble_outline),
title: Text(AppLocalizations.of(context)!.openMessage),
subtitle: Text(AppLocalizations.of(context)!.jumpToTheRelatedSarMessage),
subtitle: Text(
AppLocalizations.of(context)!.jumpToTheRelatedSarMessage,
),
onTap: () async {
Navigator.pop(sheetContext);
await _openSarMarkerMessage(
@@ -1655,7 +1664,11 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.pleaseSelectADestinationToSendSarMarker),
content: Text(
AppLocalizations.of(
context,
)!.pleaseSelectADestinationToSendSarMarker,
),
backgroundColor: Colors.red,
),
);
@@ -1785,7 +1798,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)!.sarMarkerBroadcastToPublicChannel),
content: Text(
AppLocalizations.of(context)!.sarMarkerBroadcastToPublicChannel,
),
backgroundColor: Colors.orange,
duration: Duration(seconds: 2),
),
@@ -2514,52 +2529,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
);
},
),
// Contact trail polylines (rendered before user trail and markers)
Consumer<MapProvider>(
builder: (context, mapProvider, _) {
// Determine which contacts to show trails for
final contactsToShow = mapProvider.showAllContactTrails
? contactsWithLocation // Show all when master toggle is ON
: contactsWithLocation.where(
(contact) => mapProvider.isContactPathVisible(
contact.publicKeyHex,
),
); // Individual toggles
return PolylineLayer(
polylines: contactsToShow
.where(
(contact) => contact.advertHistory.length >= 2,
)
.map((contact) {
// Use TrailColorService for consistent, emoji-based colors
final color = TrailColorService.getTrailColor(
contact,
);
return Polyline(
points: contact.advertHistory
.map((advert) => advert.location)
.toList(),
color: color.withValues(
alpha: 0.95,
), // More opaque for better visibility
strokeWidth:
4.5, // Thicker for better visibility on all map backgrounds
borderColor: Colors.white.withValues(
alpha: 0.6,
), // Stronger border contrast
borderStrokeWidth: 2.0, // Wider border
// DASHED pattern to distinguish from solid user trail
pattern: StrokePattern.dashed(
segments: [8, 4],
),
);
})
.toList(),
);
},
),
// Location trail layer (rendered after paths, before drawings)
const LocationTrailLayer(),
],
@@ -2604,7 +2573,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
context: context,
mapRotation: _getMapRotation(),
userPosition: _locationService.currentPosition,
estimatedLocations: contactsProvider.estimatedLocations,
estimatedLocations:
contactsProvider.estimatedLocations,
onTap: (contact) {
_showDetailedCompassWithContact(
context,

View File

@@ -1463,21 +1463,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
),
),
Consumer<MapProvider>(
builder: (context, mapProvider, child) => SwitchListTile(
secondary: Icon(Icons.timeline),
title: Text(
AppLocalizations.of(context)!.showAllContactTrailsLabel,
),
subtitle: const Text(
'Display location trails for all contacts that have history',
),
value: mapProvider.showAllContactTrails,
onChanged: (value) async {
await mapProvider.toggleAllContactTrails();
},
),
),
Consumer<MapProvider>(
builder: (context, mapProvider, child) => SwitchListTile(
secondary: Icon(Icons.router_outlined),

View File

@@ -64,7 +64,6 @@ class MapWorkspaceSnapshotService {
showPlaceNamesOverlay: mapProvider.showPlaceNamesOverlay,
showMunicipalityBordersOverlay:
mapProvider.showMunicipalityBordersOverlay,
showAllContactTrails: mapProvider.showAllContactTrails,
hideRepeatersOnMap: mapProvider.hideRepeatersOnMap,
);
}

View File

@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../models/contact.dart';
import '../../models/location_trail.dart';
import '../../providers/map_provider.dart';
import '../../providers/contacts_provider.dart';
import '../../services/gpx_service.dart';
import '../../services/trail_color_service.dart';
import '../../l10n/app_localizations.dart';
/// Trail management controls widget
@@ -14,17 +11,8 @@ class TrailControls extends StatelessWidget {
void _showTrailMenu(BuildContext context) {
final mapProvider = Provider.of<MapProvider>(context, listen: false);
final contactsProvider = Provider.of<ContactsProvider>(
context,
listen: false,
);
final l10n = AppLocalizations.of(context)!;
// Get contacts with trails (advertHistory >= 2 points)
final contactsWithTrails = contactsProvider.contactsWithLocation
.where((c) => c.advertHistory.length >= 2)
.toList();
showModalBottomSheet(
context: context,
isScrollControlled: true,
@@ -183,110 +171,6 @@ class TrailControls extends StatelessWidget {
),
),
const SizedBox(height: 8),
const Divider(),
const SizedBox(height: 8),
// Contact Trails Section
Row(
children: [
const Icon(Icons.people, size: 20),
SizedBox(width: 8),
Text(
l10n.contactTrails,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
// Show All Contact Trails toggle
SwitchListTile(
secondary: Icon(Icons.route),
title: Text(l10n.showAllContactTrails),
subtitle: Text(
contactsWithTrails.isEmpty
? l10n.noContactsWithLocationHistory
: mapProvider.showAllContactTrails
? l10n.showingTrailsForContacts(
contactsWithTrails.length,
)
: l10n.individualContactTrails,
),
value: mapProvider.showAllContactTrails,
onChanged: contactsWithTrails.isNotEmpty
? (value) {
mapProvider.toggleAllContactTrails();
setModalState(() {}); // Update modal UI
}
: null, // Disable if no contacts with trails
),
// Individual contact trails (when "show all" is OFF)
if (!mapProvider.showAllContactTrails &&
contactsWithTrails.isNotEmpty)
ExpansionTile(
title: Text(l10n.individualContactTrails),
initiallyExpanded: false,
children: contactsWithTrails.map((contact) {
final trailColor = TrailColorService.getTrailColor(
contact,
);
final isVisible = mapProvider.isContactPathVisible(
contact.publicKeyHex,
);
return SwitchListTile(
// Color indicator with emoji
secondary: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (contact.roleEmoji != null)
Text(
contact.roleEmoji!,
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 4),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: trailColor,
border: Border.all(
color: Colors.white,
width: 2,
),
borderRadius: BorderRadius.circular(3),
),
),
],
),
title: Row(
children: [
Expanded(child: Text(contact.displayName)),
IconButton(
tooltip: l10n.exportToClipboard,
icon: const Icon(Icons.file_download_outlined),
onPressed: () =>
_exportContactTrail(context, contact),
),
],
),
subtitle: Text(
'${contact.advertHistory.length} points',
),
value: isVisible,
onChanged: (value) {
mapProvider.toggleContactPath(contact.publicKeyHex);
setModalState(() {}); // Update modal UI
},
);
}).toList(),
),
const SizedBox(height: 8),
// Close button
@@ -360,34 +244,6 @@ class TrailControls extends StatelessWidget {
}
}
Future<void> _exportContactTrail(BuildContext context, Contact contact) async {
await _exportTrail(
context,
_buildContactTrail(contact),
customName: '${contact.displayName} Trail',
);
}
LocationTrail _buildContactTrail(Contact contact) {
final sortedHistory = [...contact.advertHistory]
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
return LocationTrail(
id: 'contact_${contact.publicKeyHex}',
isActive: false,
startTime: sortedHistory.first.timestamp,
endTime: sortedHistory.last.timestamp,
points: sortedHistory
.map(
(advert) => TrailPoint(
position: advert.location,
timestamp: advert.timestamp,
),
)
.toList(),
);
}
void _showClearConfirmation(
BuildContext context,
MapProvider mapProvider,