mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-15 15:14:26 +10:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49665fd563 | |||
| 1603adf5dd | |||
| cedbe1dd6c |
@@ -2,6 +2,8 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:crypto/crypto.dart' as crypto;
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
||||||
import 'package:pointycastle/export.dart';
|
import 'package:pointycastle/export.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
@@ -130,6 +132,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
PathHistoryService? _pathHistoryService;
|
PathHistoryService? _pathHistoryService;
|
||||||
AppSettingsService? _appSettingsService;
|
AppSettingsService? _appSettingsService;
|
||||||
BackgroundService? _backgroundService;
|
BackgroundService? _backgroundService;
|
||||||
|
SparseLocationLogger? _sparseLocationLogger;
|
||||||
final NotificationService _notificationService = NotificationService();
|
final NotificationService _notificationService = NotificationService();
|
||||||
BleDebugLogService? _bleDebugLogService;
|
BleDebugLogService? _bleDebugLogService;
|
||||||
AppDebugLogService? _appDebugLogService;
|
AppDebugLogService? _appDebugLogService;
|
||||||
@@ -502,6 +505,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
BleDebugLogService? bleDebugLogService,
|
BleDebugLogService? bleDebugLogService,
|
||||||
AppDebugLogService? appDebugLogService,
|
AppDebugLogService? appDebugLogService,
|
||||||
BackgroundService? backgroundService,
|
BackgroundService? backgroundService,
|
||||||
|
SparseLocationLogger? sparseLocationLogger,
|
||||||
}) {
|
}) {
|
||||||
_retryService = retryService;
|
_retryService = retryService;
|
||||||
_pathHistoryService = pathHistoryService;
|
_pathHistoryService = pathHistoryService;
|
||||||
@@ -509,11 +513,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_bleDebugLogService = bleDebugLogService;
|
_bleDebugLogService = bleDebugLogService;
|
||||||
_appDebugLogService = appDebugLogService;
|
_appDebugLogService = appDebugLogService;
|
||||||
_backgroundService = backgroundService;
|
_backgroundService = backgroundService;
|
||||||
|
_sparseLocationLogger = sparseLocationLogger;
|
||||||
|
|
||||||
// Initialize notification service
|
// Initialize notification service
|
||||||
_notificationService.initialize();
|
_notificationService.initialize();
|
||||||
_loadChannelOrder();
|
_loadChannelOrder();
|
||||||
|
|
||||||
|
_sparseLocationLogger?.initialize(_updateLocationandAdvert);
|
||||||
|
|
||||||
// Initialize retry service callbacks
|
// Initialize retry service callbacks
|
||||||
_retryService?.initialize(
|
_retryService?.initialize(
|
||||||
sendMessageCallback: _sendMessageDirect,
|
sendMessageCallback: _sendMessageDirect,
|
||||||
@@ -828,6 +835,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SparseLocationLogger? get sparseLocationLogger => _sparseLocationLogger;
|
||||||
|
|
||||||
bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
|
bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
|
||||||
|
|
||||||
void _cancelReconnectTimer() {
|
void _cancelReconnectTimer() {
|
||||||
@@ -1690,6 +1699,11 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
_isLoadingContacts = true;
|
_isLoadingContacts = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
break;
|
break;
|
||||||
|
case pushCodeNewAdvert:
|
||||||
|
debugPrint('Got New CONTACT');
|
||||||
|
// It the same format as respCodeContact, so we can reuse the handler
|
||||||
|
_handleContact(frame);
|
||||||
|
break;
|
||||||
case respCodeContact:
|
case respCodeContact:
|
||||||
debugPrint('Got CONTACT');
|
debugPrint('Got CONTACT');
|
||||||
_handleContact(frame);
|
_handleContact(frame);
|
||||||
@@ -1734,6 +1748,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
case pushCodeStatusResponse:
|
case pushCodeStatusResponse:
|
||||||
break;
|
break;
|
||||||
case pushCodeLogRxData:
|
case pushCodeLogRxData:
|
||||||
|
_handleRxData(frame);
|
||||||
_handleLogRxData(frame);
|
_handleLogRxData(frame);
|
||||||
break;
|
break;
|
||||||
case respCodeChannelInfo:
|
case respCodeChannelInfo:
|
||||||
@@ -1747,6 +1762,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
case respCodeCustomVars:
|
case respCodeCustomVars:
|
||||||
_handleCustomVars(frame);
|
_handleCustomVars(frame);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
debugPrint('Unknown frame code: $code');
|
debugPrint('Unknown frame code: $code');
|
||||||
}
|
}
|
||||||
@@ -2002,6 +2018,76 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleContactAdvert(Contact contact) {
|
||||||
|
if (contact.type == advTypeRepeater) {
|
||||||
|
_contactUnreadCount.remove(contact.publicKeyHex);
|
||||||
|
_unreadStore.saveContactUnreadCount(
|
||||||
|
Map<String, int>.from(_contactUnreadCount),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Check if this is a new contact
|
||||||
|
final isNewContact = !_knownContactKeys.contains(contact.publicKeyHex);
|
||||||
|
final existingIndex = _contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
final existing = _contacts[existingIndex];
|
||||||
|
final mergedLastMessageAt =
|
||||||
|
existing.lastMessageAt.isAfter(contact.lastMessageAt)
|
||||||
|
? existing.lastMessageAt
|
||||||
|
: contact.lastMessageAt;
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
|
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||||
|
_contacts[existingIndex] = contact.copyWith(
|
||||||
|
lastMessageAt: mergedLastMessageAt,
|
||||||
|
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||||
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_contacts.add(contact);
|
||||||
|
appLogger.info(
|
||||||
|
'Added new contact ${contact.name}: pathLen=${contact.pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_knownContactKeys.add(contact.publicKeyHex);
|
||||||
|
_loadMessagesForContact(contact.publicKeyHex);
|
||||||
|
|
||||||
|
// Add path to history if we have a valid path
|
||||||
|
if (_pathHistoryService != null && contact.pathLength >= 0) {
|
||||||
|
_pathHistoryService!.handlePathUpdated(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
// Show notification for new contact (advertisement)
|
||||||
|
if (isNewContact && _appSettingsService != null) {
|
||||||
|
final settings = _appSettingsService!.settings;
|
||||||
|
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
|
||||||
|
_notificationService.showAdvertNotification(
|
||||||
|
contactName: contact.name,
|
||||||
|
contactType: contact.typeLabel,
|
||||||
|
contactId: contact.publicKeyHex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isLoadingContacts) {
|
||||||
|
unawaited(_persistContacts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _persistContacts() async {
|
Future<void> _persistContacts() async {
|
||||||
await _contactStore.saveContacts(_contacts);
|
await _contactStore.saveContacts(_contacts);
|
||||||
}
|
}
|
||||||
@@ -3285,6 +3371,136 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateLocationandAdvert(Position position) async {
|
||||||
|
final snapToGridCenter = _sparseLocationLogger?.snapToGridCenter(
|
||||||
|
position: position,
|
||||||
|
cellSizeMeters: 0.001,
|
||||||
|
);
|
||||||
|
double lat = snapToGridCenter?.latitude ?? 0.0;
|
||||||
|
double lon = snapToGridCenter?.longitude ?? 0.0;
|
||||||
|
|
||||||
|
if (lat == 0.0 && lon == 0.0) {
|
||||||
|
debugPrint('Invalid location (0,0), skipping advert');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendFrame(buildSetOtherParamsFrame(true, 1, 1, 0));
|
||||||
|
await setNodeLocation(lat: lat, lon: lon);
|
||||||
|
await sendSelfAdvert(flood: true);
|
||||||
|
_selfLatitude = lat;
|
||||||
|
_selfLongitude = lon;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleRxData(Uint8List frame) {
|
||||||
|
final packet = BufferReader(frame);
|
||||||
|
packet.skipBytes(3); // Skip frame type byte
|
||||||
|
//final snr = packet.readByte() / 4.0;
|
||||||
|
//final rssi = packet.readByte();
|
||||||
|
final header = packet.readByte();
|
||||||
|
//final routeType = header & 0x03;
|
||||||
|
final payloadType = (header >> 2) & 0x0F;
|
||||||
|
//final payloadVer = (header >> 6) & 0x03;
|
||||||
|
|
||||||
|
if (packet.remaining <= 0) return;
|
||||||
|
final pathLen = packet.readByte();
|
||||||
|
|
||||||
|
if (packet.remaining < pathLen) return;
|
||||||
|
final pathBytes = packet.readBytes(pathLen);
|
||||||
|
|
||||||
|
if (packet.remaining <= 0) return;
|
||||||
|
final payload = packet.readBytes(packet.remaining);
|
||||||
|
|
||||||
|
switch (payloadType) {
|
||||||
|
case payloadTypeADVERT:
|
||||||
|
_handlePayloadAdvertReceived(payload, pathBytes);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePayloadAdvertReceived(Uint8List frame, Uint8List path) {
|
||||||
|
final advert = BufferReader(frame);
|
||||||
|
if (advert.remaining <= 32) return;
|
||||||
|
final publicKey = advert.readBytes(32);
|
||||||
|
final contactKeyHex = publicKey
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
if (advert.remaining <= 4) return;
|
||||||
|
final timestamp = advert.readInt32LE();
|
||||||
|
if (advert.remaining <= 64) return;
|
||||||
|
advert.skipBytes(64); // Skip signature for now
|
||||||
|
if (advert.remaining <= 1) return;
|
||||||
|
final flags = advert.readByte();
|
||||||
|
final type = flags & 0x0F;
|
||||||
|
final hasLocation = (flags & 0x10) != 0;
|
||||||
|
//final hasFeature1 = (flags & 0x20) != 0;
|
||||||
|
//final hasFeature2 = (flags & 0x40) != 0;
|
||||||
|
final hasName = (flags & 0x80) != 0;
|
||||||
|
double latitude = 0.0;
|
||||||
|
double longitude = 0.0;
|
||||||
|
if (hasLocation && advert.remaining >= 8) {
|
||||||
|
latitude = advert.readInt32LE() / 1e6;
|
||||||
|
longitude = advert.readInt32LE() / 1e6;
|
||||||
|
}
|
||||||
|
String name = '';
|
||||||
|
if (hasName && advert.remaining > 0) {
|
||||||
|
name = advert.readString();
|
||||||
|
}
|
||||||
|
// Check if this is a new contact
|
||||||
|
final isNewContact = !_knownContactKeys.contains(contactKeyHex);
|
||||||
|
if (isNewContact) {
|
||||||
|
_handleContactAdvert(
|
||||||
|
Contact(
|
||||||
|
publicKey: publicKey,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
pathLength: path.length,
|
||||||
|
path: path,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingIndex = _contacts.indexWhere(
|
||||||
|
(c) => c.publicKeyHex == contactKeyHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
final existing = _contacts[existingIndex];
|
||||||
|
final mergedLastMessageAt = existing.lastMessageAt.isAfter(DateTime.now())
|
||||||
|
? DateTime.now()
|
||||||
|
: existing.lastMessageAt;
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
|
||||||
|
// CRITICAL: Preserve user's path override when contact is refreshed from device
|
||||||
|
_contacts[existingIndex] = existing.copyWith(
|
||||||
|
latitude: hasLocation ? latitude : existing.latitude,
|
||||||
|
longitude: hasLocation ? longitude : existing.longitude,
|
||||||
|
name: hasName ? name : existing.name,
|
||||||
|
path: path,
|
||||||
|
pathLength: path.length,
|
||||||
|
lastMessageAt: mergedLastMessageAt,
|
||||||
|
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
|
||||||
|
pathOverride: existing.pathOverride, // Preserve user's path choice
|
||||||
|
pathOverrideBytes: existing.pathOverrideBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
appLogger.info(
|
||||||
|
'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}',
|
||||||
|
tag: 'Connector',
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const int _phRouteMask = 0x03;
|
const int _phRouteMask = 0x03;
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ const int cmdGetContactByKey = 30;
|
|||||||
const int cmdGetChannel = 31;
|
const int cmdGetChannel = 31;
|
||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdSendTracePath = 36;
|
const int cmdSendTracePath = 36;
|
||||||
|
const int cmdSetOtherParams = 38;
|
||||||
const int cmdGetRadioSettings = 57;
|
const int cmdGetRadioSettings = 57;
|
||||||
const int cmdGetTelemetryReq = 39;
|
const int cmdGetTelemetryReq = 39;
|
||||||
const int cmdGetCustomVar = 40;
|
const int cmdGetCustomVar = 40;
|
||||||
@@ -212,6 +213,30 @@ const int advTypeRepeater = 2;
|
|||||||
const int advTypeRoom = 3;
|
const int advTypeRoom = 3;
|
||||||
const int advTypeSensor = 4;
|
const int advTypeSensor = 4;
|
||||||
|
|
||||||
|
// Payload Types
|
||||||
|
const int payloadTypeREQ =
|
||||||
|
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeRESPONSE =
|
||||||
|
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeTXTMSG =
|
||||||
|
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
|
||||||
|
const int payloadTypeACK = 0x03; // a simple ack
|
||||||
|
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
|
||||||
|
const int payloadTypeGRPTXT =
|
||||||
|
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
|
||||||
|
const int payloadTypeGRPDATA =
|
||||||
|
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
|
||||||
|
const int payloadTypeANONREQ =
|
||||||
|
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||||
|
const int payloadTypePATH =
|
||||||
|
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||||
|
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
|
||||||
|
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
|
||||||
|
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||||
|
//...
|
||||||
|
const int payloadTypeRawCustom =
|
||||||
|
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||||
|
|
||||||
// Sizes
|
// Sizes
|
||||||
const int pubKeySize = 32;
|
const int pubKeySize = 32;
|
||||||
const int maxPathSize = 64;
|
const int maxPathSize = 64;
|
||||||
@@ -777,3 +802,22 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
|||||||
writer.writeBytes(pubKey);
|
writer.writeBytes(pubKey);
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build CMD_SET_OTHER_PARAMS frame
|
||||||
|
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
|
||||||
|
Uint8List buildSetOtherParamsFrame(
|
||||||
|
bool allowAutoAddContacts,
|
||||||
|
int allowTelemetryFlags,
|
||||||
|
int advertLocationPolicy,
|
||||||
|
int multiAcks,
|
||||||
|
) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSetOtherParams);
|
||||||
|
writer.writeByte(
|
||||||
|
allowAutoAddContacts ? 0x00 : 0x01,
|
||||||
|
); // Allow Auto Add Contacts
|
||||||
|
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||||
|
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
|
||||||
|
writer.writeByte(multiAcks); // Multi Acknowledgements
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|||||||
@@ -619,6 +619,7 @@
|
|||||||
"map_sharedPin": "Shared pin",
|
"map_sharedPin": "Shared pin",
|
||||||
"map_joinRoom": "Join Room",
|
"map_joinRoom": "Join Room",
|
||||||
"map_manageRepeater": "Manage Repeater",
|
"map_manageRepeater": "Manage Repeater",
|
||||||
|
"map_updateMyLocation": "Update Location",
|
||||||
"mapCache_title": "Offline Map Cache",
|
"mapCache_title": "Offline Map Cache",
|
||||||
"mapCache_selectAreaFirst": "Select an area to cache first",
|
"mapCache_selectAreaFirst": "Select an area to cache first",
|
||||||
"mapCache_noTilesToDownload": "No tiles to download for this area",
|
"mapCache_noTilesToDownload": "No tiles to download for this area",
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ void main() async {
|
|||||||
final appDebugLogService = AppDebugLogService();
|
final appDebugLogService = AppDebugLogService();
|
||||||
final backgroundService = BackgroundService();
|
final backgroundService = BackgroundService();
|
||||||
final mapTileCacheService = MapTileCacheService();
|
final mapTileCacheService = MapTileCacheService();
|
||||||
|
final sparseLocationLogger = SparseLocationLogger();
|
||||||
// Load settings
|
// Load settings
|
||||||
await appSettingsService.loadSettings();
|
await appSettingsService.loadSettings();
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ void main() async {
|
|||||||
bleDebugLogService: bleDebugLogService,
|
bleDebugLogService: bleDebugLogService,
|
||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
backgroundService: backgroundService,
|
backgroundService: backgroundService,
|
||||||
|
sparseLocationLogger: sparseLocationLogger,
|
||||||
);
|
);
|
||||||
|
|
||||||
await connector.loadContactCache();
|
await connector.loadContactCache();
|
||||||
@@ -76,6 +78,7 @@ void main() async {
|
|||||||
bleDebugLogService: bleDebugLogService,
|
bleDebugLogService: bleDebugLogService,
|
||||||
appDebugLogService: appDebugLogService,
|
appDebugLogService: appDebugLogService,
|
||||||
mapTileCacheService: mapTileCacheService,
|
mapTileCacheService: mapTileCacheService,
|
||||||
|
sparseLocationLogger: sparseLocationLogger,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -89,6 +92,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
final BleDebugLogService bleDebugLogService;
|
final BleDebugLogService bleDebugLogService;
|
||||||
final AppDebugLogService appDebugLogService;
|
final AppDebugLogService appDebugLogService;
|
||||||
final MapTileCacheService mapTileCacheService;
|
final MapTileCacheService mapTileCacheService;
|
||||||
|
final SparseLocationLogger sparseLocationLogger;
|
||||||
|
|
||||||
const MeshCoreApp({
|
const MeshCoreApp({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -100,6 +104,7 @@ class MeshCoreApp extends StatelessWidget {
|
|||||||
required this.bleDebugLogService,
|
required this.bleDebugLogService,
|
||||||
required this.appDebugLogService,
|
required this.appDebugLogService,
|
||||||
required this.mapTileCacheService,
|
required this.mapTileCacheService,
|
||||||
|
required this.sparseLocationLogger,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -275,6 +275,17 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.refresh),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(context.l10n.map_updateMyLocation),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () =>
|
||||||
|
connector.sparseLocationLogger?.updateMyLocation(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
@@ -357,8 +368,8 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
connector.selfLatitude!,
|
connector.selfLatitude!,
|
||||||
connector.selfLongitude!,
|
connector.selfLongitude!,
|
||||||
),
|
),
|
||||||
width: 35,
|
width: 40,
|
||||||
height: 35,
|
height: 40,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -490,6 +490,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final latController = TextEditingController();
|
final latController = TextEditingController();
|
||||||
final lonController = TextEditingController();
|
final lonController = TextEditingController();
|
||||||
final intervalController = TextEditingController();
|
final intervalController = TextEditingController();
|
||||||
|
bool isLogging = connector.sparseLocationLogger?.isLogging() ?? false;
|
||||||
|
|
||||||
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
|
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
|
||||||
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
|
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
|
||||||
|
|
||||||
@@ -534,6 +536,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
signed: true,
|
signed: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FeatureToggleRow(
|
||||||
|
title: "GPS Logging",
|
||||||
|
subtitle: "Enable GPS logging on the device",
|
||||||
|
value: isLogging,
|
||||||
|
onChanged: (value) async {
|
||||||
|
setDialogState(() => isLogging = value);
|
||||||
|
if (value) {
|
||||||
|
await connector.sparseLocationLogger?.startLogging();
|
||||||
|
} else {
|
||||||
|
await connector.sparseLocationLogger?.stopLogging();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
if (hasGPS) ...[
|
if (hasGPS) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:gpx/gpx.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
class SparseLocationLogger {
|
||||||
|
static const double distanceThresholdMiles = 0.25;
|
||||||
|
static const double distanceThresholdMeters =
|
||||||
|
distanceThresholdMiles * 1609.34;
|
||||||
|
static const double headingChangeThresholdDeg = 35.0;
|
||||||
|
static const double minSpeedForTurnKmh = 8.0;
|
||||||
|
static const double minTime = 120.0; // seconds
|
||||||
|
|
||||||
|
Position? _lastLoggedPosition;
|
||||||
|
double? _lastHeading;
|
||||||
|
DateTime? _lastLoggedTime;
|
||||||
|
StreamSubscription<Position>? _positionStream;
|
||||||
|
Timer? _timer;
|
||||||
|
Function(Position position)? _onNewLogPoint;
|
||||||
|
// GPX structures
|
||||||
|
final Gpx _gpx = Gpx();
|
||||||
|
Trkseg _currentSegment = Trkseg(); // one segment for the whole session
|
||||||
|
|
||||||
|
File? _gpxFile;
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
void initialize(Function(Position position) onNewLogPoint) {
|
||||||
|
_onNewLogPoint = onNewLogPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getPremissons() async {
|
||||||
|
// Permissions & service check (same as before)
|
||||||
|
var status = await Permission.location.request();
|
||||||
|
if (!status.isGranted) {
|
||||||
|
debugPrint('Location permission denied');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
|
if (!serviceEnabled) {
|
||||||
|
debugPrint('Location services disabled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startLogging() async {
|
||||||
|
if (!await getPremissons()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare files
|
||||||
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
|
final timestamp = DateTime.now()
|
||||||
|
.toIso8601String()
|
||||||
|
.replaceAll(':', '-')
|
||||||
|
.split('.')
|
||||||
|
.first;
|
||||||
|
_gpxFile = File('${directory.path}/track_$timestamp.gpx');
|
||||||
|
|
||||||
|
// Init GPX metadata
|
||||||
|
_gpx.metadata = Metadata(
|
||||||
|
name: 'Sparse Track ${DateTime.now().toString().split(' ').first}',
|
||||||
|
desc: 'Sparse GPS log: ~every 1.5 mi or significant turns',
|
||||||
|
time: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add one track with one segment
|
||||||
|
final track = Trk(name: 'Main Track');
|
||||||
|
_currentSegment = Trkseg();
|
||||||
|
track.trksegs.add(_currentSegment);
|
||||||
|
_gpx.trks.add(track);
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
|
||||||
|
// Start location stream
|
||||||
|
_positionStream = Geolocator.getPositionStream(
|
||||||
|
locationSettings: const LocationSettings(
|
||||||
|
accuracy: LocationAccuracy.high,
|
||||||
|
distanceFilter: 152, // meters (~0.16 mi) - helps battery
|
||||||
|
),
|
||||||
|
).listen(_onPositionReceived);
|
||||||
|
|
||||||
|
// Also poll via timer as fallback
|
||||||
|
_timer = Timer.periodic(Duration(seconds: (minTime / 2).toInt()), (
|
||||||
|
_,
|
||||||
|
) async {
|
||||||
|
final position = await Geolocator.getCurrentPosition();
|
||||||
|
await _onPositionReceived(position);
|
||||||
|
});
|
||||||
|
|
||||||
|
_lastLoggedPosition = null;
|
||||||
|
_lastHeading = null;
|
||||||
|
_lastLoggedTime = null;
|
||||||
|
|
||||||
|
debugPrint('Sparse GPX logging started → ${_gpxFile?.path}');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopLogging() async {
|
||||||
|
await _positionStream?.cancel();
|
||||||
|
_positionStream = null;
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
|
||||||
|
if (_isInitialized && _currentSegment.trkpts.isNotEmpty) {
|
||||||
|
// Write GPX file on stop
|
||||||
|
final xmlString = GpxWriter().asString(_gpx, pretty: true);
|
||||||
|
|
||||||
|
await _gpxFile?.writeAsString(xmlString);
|
||||||
|
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
text: 'Sparse GPS track',
|
||||||
|
subject: 'Sparse GPS track',
|
||||||
|
files: [XFile(_gpxFile?.path ?? '')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _gpxFile?.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('Logging stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateMyLocation() async {
|
||||||
|
if (!await getPremissons()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final position = await Geolocator.getCurrentPosition();
|
||||||
|
_onNewLogPoint?.call(position);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error updating location: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onPositionReceived(Position position) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final speedKmh = position.speed * 3.6;
|
||||||
|
final heading = position.heading;
|
||||||
|
|
||||||
|
bool shouldLog = false;
|
||||||
|
String reason = '';
|
||||||
|
|
||||||
|
if (_lastLoggedPosition == null) {
|
||||||
|
shouldLog = true;
|
||||||
|
reason = 'start';
|
||||||
|
} else {
|
||||||
|
final distanceMeters = Geolocator.distanceBetween(
|
||||||
|
_lastLoggedPosition!.latitude,
|
||||||
|
_lastLoggedPosition!.longitude,
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distanceMeters >= distanceThresholdMeters) {
|
||||||
|
shouldLog = true;
|
||||||
|
reason =
|
||||||
|
'distance (${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
|
||||||
|
} else if (speedKmh > minSpeedForTurnKmh && _lastHeading != null) {
|
||||||
|
double delta = (heading - _lastHeading!).abs();
|
||||||
|
delta = math.min(delta, 360 - delta);
|
||||||
|
if (delta > headingChangeThresholdDeg) {
|
||||||
|
shouldLog = true;
|
||||||
|
reason = 'turn (${delta.toStringAsFixed(1)}°)';
|
||||||
|
}
|
||||||
|
} else if (_lastLoggedTime != null) {
|
||||||
|
final elapsed = now.difference(_lastLoggedTime!).inSeconds;
|
||||||
|
if (elapsed >= minTime && distanceMeters >= distanceThresholdMeters) {
|
||||||
|
shouldLog = true;
|
||||||
|
reason = 'time (${elapsed}s)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldLog) {
|
||||||
|
// Create GPX Waypoint (trkpt)
|
||||||
|
final pt = Wpt(
|
||||||
|
lat: position.latitude,
|
||||||
|
lon: position.longitude,
|
||||||
|
ele: position.altitude, // if available
|
||||||
|
time: now,
|
||||||
|
extensions: {
|
||||||
|
"course": ?heading.isFinite ? heading : null,
|
||||||
|
"speed": ?speedKmh > 0 ? speedKmh / 3.6 : null, // GPX speed in m/s
|
||||||
|
},
|
||||||
|
// You can add hdop, vdop, etc. from position if desired
|
||||||
|
);
|
||||||
|
|
||||||
|
_currentSegment.trkpts.add(pt);
|
||||||
|
_onNewLogPoint?.call(position);
|
||||||
|
debugPrint('Logged point: ${pt.lat}, ${pt.lon} ($reason)');
|
||||||
|
|
||||||
|
_lastLoggedPosition = position;
|
||||||
|
_lastHeading = heading;
|
||||||
|
_lastLoggedTime = now;
|
||||||
|
} else {
|
||||||
|
debugPrint('Skipped point: ${position.latitude}, ${position.longitude}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Position snapToGridCenter({
|
||||||
|
required Position position,
|
||||||
|
required double cellSizeMeters,
|
||||||
|
}) {
|
||||||
|
Position snappedPosition = position;
|
||||||
|
// Snap latitude
|
||||||
|
final latFloor =
|
||||||
|
(position.latitude / cellSizeMeters).floor() * cellSizeMeters;
|
||||||
|
final snappedLat = latFloor + (cellSizeMeters / 2);
|
||||||
|
|
||||||
|
// Snap longitude
|
||||||
|
final lonFloor =
|
||||||
|
(position.longitude / cellSizeMeters).floor() * cellSizeMeters;
|
||||||
|
final snappedLon = lonFloor + (cellSizeMeters / 2);
|
||||||
|
|
||||||
|
snappedPosition = Position(
|
||||||
|
latitude: snappedLat,
|
||||||
|
longitude: snappedLon,
|
||||||
|
altitude: position.altitude,
|
||||||
|
accuracy: position.accuracy,
|
||||||
|
heading: position.heading,
|
||||||
|
speed: position.speed,
|
||||||
|
speedAccuracy: position.speedAccuracy,
|
||||||
|
altitudeAccuracy: position.altitudeAccuracy,
|
||||||
|
headingAccuracy: position.headingAccuracy,
|
||||||
|
timestamp: position.timestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
return snappedPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getGpxFilePath() async => _gpxFile?.path ?? 'Not started';
|
||||||
|
bool isLogging() => _positionStream != null;
|
||||||
|
int getPointCount() => _currentSegment.trkpts.length;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import Foundation
|
|||||||
|
|
||||||
import flutter_blue_plus_darwin
|
import flutter_blue_plus_darwin
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
|
import geolocator_apple
|
||||||
import mobile_scanner
|
import mobile_scanner
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import share_plus
|
import share_plus
|
||||||
@@ -18,6 +19,7 @@ import wakelock_plus
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
|||||||
+130
-10
@@ -69,10 +69,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -341,6 +341,70 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
geoclue:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geoclue
|
||||||
|
sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
|
geolocator:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: geolocator
|
||||||
|
sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.0.2"
|
||||||
|
geolocator_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_android
|
||||||
|
sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.2"
|
||||||
|
geolocator_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_apple
|
||||||
|
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.13"
|
||||||
|
geolocator_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_linux
|
||||||
|
sha256: c4e966f0a7a87e70049eac7a2617f9e16fd4c585a26e4330bdfc3a71e6a721f3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.3"
|
||||||
|
geolocator_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_platform_interface
|
||||||
|
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.6"
|
||||||
|
geolocator_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_web
|
||||||
|
sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.3"
|
||||||
|
geolocator_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: geolocator_windows
|
||||||
|
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.5"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -357,6 +421,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
gsettings:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: gsettings
|
||||||
|
sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.8"
|
||||||
hooks:
|
hooks:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -489,26 +561,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.17.0"
|
||||||
mgrs_dart:
|
mgrs_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -637,6 +709,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.0.1"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.7"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -910,10 +1030,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.7"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ dependencies:
|
|||||||
gpx: ^2.3.0
|
gpx: ^2.3.0
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
share_plus: ^12.0.1
|
share_plus: ^12.0.1
|
||||||
|
geolocator: ^14.0.2
|
||||||
|
permission_handler: ^12.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -7,12 +7,18 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <flutter_blue_plus_winrt/flutter_blue_plus_plugin.h>
|
#include <flutter_blue_plus_winrt/flutter_blue_plus_plugin.h>
|
||||||
|
#include <geolocator_windows/geolocator_windows.h>
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FlutterBluePlusPluginRegisterWithRegistrar(
|
FlutterBluePlusPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterBluePlusPlugin"));
|
registry->GetRegistrarForPlugin("FlutterBluePlusPlugin"));
|
||||||
|
GeolocatorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
flutter_blue_plus_winrt
|
flutter_blue_plus_winrt
|
||||||
|
geolocator_windows
|
||||||
|
permission_handler_windows
|
||||||
share_plus
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user