mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add compass and region support
This commit is contained in:
11
lib/services/compass_support.dart
Normal file
11
lib/services/compass_support.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter/foundation.dart'
|
||||
show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||
|
||||
class CompassSupport {
|
||||
const CompassSupport._();
|
||||
|
||||
static bool get isAvailable =>
|
||||
!kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS);
|
||||
}
|
||||
72
lib/services/region_discovery_service.dart
Normal file
72
lib/services/region_discovery_service.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
|
||||
/// Discovers available regions from repeater contacts via anonymous requests.
|
||||
///
|
||||
/// The firmware repeater responds to ANON_REQ_TYPE_REGIONS (0x01) with a
|
||||
/// comma-separated list of region names that have flood allowed.
|
||||
class RegionDiscoveryService {
|
||||
static const int _anonReqTypeRegions = 0x01;
|
||||
|
||||
/// Discover regions from a single repeater.
|
||||
///
|
||||
/// Sends an anonymous request to the repeater and waits for the response.
|
||||
/// Returns a list of region names (with `#` prefix).
|
||||
/// Returns empty list on timeout or error.
|
||||
static Future<List<String>> discoverFromRepeater({
|
||||
required Uint8List repeaterPublicKey,
|
||||
required ConnectionProvider connectionProvider,
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final result = await connectionProvider.sendAnonRequest(
|
||||
contactPublicKey: repeaterPublicKey,
|
||||
requestData: Uint8List.fromList([_anonReqTypeRegions]),
|
||||
);
|
||||
if (result == null) return [];
|
||||
|
||||
final tag = result.tag;
|
||||
final completer = Completer<List<String>>();
|
||||
|
||||
void onResponse(Uint8List publicKeyPrefix, int responseTag, Uint8List data) {
|
||||
if (responseTag != tag || completer.isCompleted) return;
|
||||
completer.complete(_parseRegionResponse(data));
|
||||
}
|
||||
|
||||
connectionProvider.onBinaryResponse = onResponse;
|
||||
|
||||
try {
|
||||
return await completer.future.timeout(
|
||||
timeout,
|
||||
onTimeout: () => <String>[],
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [RegionDiscovery] Error discovering regions: $e');
|
||||
return [];
|
||||
} finally {
|
||||
// Restore previous handler — callers should re-set if needed
|
||||
if (connectionProvider.onBinaryResponse == onResponse) {
|
||||
connectionProvider.onBinaryResponse = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the region response payload.
|
||||
///
|
||||
/// Format: [4B sender_timestamp][4B repeater_clock][comma-separated names]
|
||||
/// Names are returned without `#` prefix from firmware; we add it back.
|
||||
static List<String> _parseRegionResponse(Uint8List data) {
|
||||
if (data.length <= 8) return [];
|
||||
|
||||
final namesStr = utf8.decode(data.sublist(8), allowMalformed: true).trim();
|
||||
if (namesStr.isEmpty || namesStr == '-none-') return [];
|
||||
|
||||
return namesStr
|
||||
.split(',')
|
||||
.map((name) => name.trim())
|
||||
.where((name) => name.isNotEmpty && name != '*' && !name.startsWith('\$'))
|
||||
.map((name) => name.startsWith('#') ? name : '#$name')
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
86
lib/services/region_scope_preferences.dart
Normal file
86
lib/services/region_scope_preferences.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Stores per-channel region scope settings.
|
||||
///
|
||||
/// Each channel can optionally have a region scope assigned. When set, the app
|
||||
/// will set the flood scope on the device before sending channel messages so
|
||||
/// that repeaters outside the region won't forward them.
|
||||
class RegionScopePreferences {
|
||||
static const String _namePrefix = 'region_scope_name_';
|
||||
static const String _keyPrefix = 'region_scope_key_';
|
||||
|
||||
/// Get the saved scope for a channel.
|
||||
/// Returns null if no scope is set.
|
||||
static Future<({String name, Uint8List key})?> getScope(
|
||||
int channelIdx,
|
||||
) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final name = prefs.getString(
|
||||
ProfileStorageScope.scopedKey('$_namePrefix$channelIdx'),
|
||||
);
|
||||
final keyBase64 = prefs.getString(
|
||||
ProfileStorageScope.scopedKey('$_keyPrefix$channelIdx'),
|
||||
);
|
||||
if (name == null || keyBase64 == null) return null;
|
||||
try {
|
||||
return (name: name, key: Uint8List.fromList(base64.decode(keyBase64)));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Save a region scope for a channel.
|
||||
static Future<void> setScope(
|
||||
int channelIdx,
|
||||
String name,
|
||||
Uint8List key,
|
||||
) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
ProfileStorageScope.scopedKey('$_namePrefix$channelIdx'),
|
||||
name,
|
||||
);
|
||||
await prefs.setString(
|
||||
ProfileStorageScope.scopedKey('$_keyPrefix$channelIdx'),
|
||||
base64.encode(key),
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear the region scope for a channel.
|
||||
static Future<void> clearScope(int channelIdx) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(
|
||||
ProfileStorageScope.scopedKey('$_namePrefix$channelIdx'),
|
||||
);
|
||||
await prefs.remove(
|
||||
ProfileStorageScope.scopedKey('$_keyPrefix$channelIdx'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear all region scopes (all channels).
|
||||
static Future<void> clearAllScopes() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final keys = prefs.getKeys();
|
||||
for (final key in keys) {
|
||||
if (key.contains(_namePrefix) || key.contains(_keyPrefix)) {
|
||||
await prefs.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the 16-byte transport key for a region name.
|
||||
///
|
||||
/// Matches firmware `TransportKeyStore::getAutoKeyFor`:
|
||||
/// `SHA256("#regionname")` → first 16 bytes.
|
||||
/// The name must include the leading `#` (auto-prepended if missing).
|
||||
static Uint8List deriveRegionKey(String regionName) {
|
||||
final normalized =
|
||||
regionName.startsWith('#') ? regionName : '#$regionName';
|
||||
final digest = sha256.convert(utf8.encode(normalized));
|
||||
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user