mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-28 05:07:31 +10:00
remove voice code make optimizations. Fix channels race conditions. add reply function
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../storage/prefs_manager.dart';
|
||||
|
||||
class AppSettingsService extends ChangeNotifier {
|
||||
static const String _settingsKey = 'app_settings';
|
||||
@@ -17,7 +17,7 @@ class AppSettingsService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> loadSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_settingsKey);
|
||||
|
||||
if (jsonStr != null) {
|
||||
@@ -36,7 +36,7 @@ class AppSettingsService extends ChangeNotifier {
|
||||
_settings = newSettings;
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = jsonEncode(_settings.toJson());
|
||||
await prefs.setString(_settingsKey, jsonStr);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../storage/prefs_manager.dart';
|
||||
|
||||
class MapMarkerService {
|
||||
static const String _removedKey = 'map_removed_marker_ids';
|
||||
|
||||
Future<Set<String>> loadRemovedIds() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final items = prefs.getStringList(_removedKey) ?? const [];
|
||||
return items.toSet();
|
||||
}
|
||||
|
||||
Future<void> saveRemovedIds(Set<String> ids) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
await prefs.setStringList(_removedKey, ids.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ class PathHistoryService extends ChangeNotifier {
|
||||
final Map<String, int> _autoRotationIndex = {};
|
||||
final Map<String, _FloodStats> _floodStats = {};
|
||||
|
||||
// LRU cache eviction tracking
|
||||
static const int _maxCachedContacts = 50;
|
||||
final List<String> _cacheAccessOrder = [];
|
||||
|
||||
static const int _maxHistoryEntries = 100;
|
||||
static const int _autoRotationTopCount = 3;
|
||||
|
||||
@@ -91,6 +95,8 @@ class PathHistoryService extends ChangeNotifier {
|
||||
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
|
||||
}
|
||||
|
||||
_trackAccess(contactPubKeyHex);
|
||||
|
||||
final selections = ranked
|
||||
.map((path) => PathSelection(
|
||||
pathBytes: path.pathBytes,
|
||||
@@ -208,6 +214,8 @@ class PathHistoryService extends ChangeNotifier {
|
||||
);
|
||||
|
||||
_cache[contactPubKeyHex] = updatedHistory;
|
||||
_trackAccess(contactPubKeyHex);
|
||||
_evictIfNeeded();
|
||||
_storage.savePathHistory(contactPubKeyHex, updatedHistory);
|
||||
|
||||
notifyListeners();
|
||||
@@ -216,12 +224,15 @@ class PathHistoryService extends ChangeNotifier {
|
||||
List<PathRecord> getRecentPaths(String contactPubKeyHex) {
|
||||
final history = _cache[contactPubKeyHex];
|
||||
if (history != null) {
|
||||
_trackAccess(contactPubKeyHex);
|
||||
return history.recentPaths;
|
||||
}
|
||||
|
||||
_loadHistoryFromStorage(contactPubKeyHex).then((loaded) {
|
||||
if (loaded != null) {
|
||||
_cache[contactPubKeyHex] = loaded;
|
||||
_trackAccess(contactPubKeyHex);
|
||||
_evictIfNeeded();
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
@@ -236,16 +247,23 @@ class PathHistoryService extends ChangeNotifier {
|
||||
|
||||
PathRecord? getFastestPath(String contactPubKeyHex) {
|
||||
final history = _cache[contactPubKeyHex];
|
||||
if (history != null) {
|
||||
_trackAccess(contactPubKeyHex);
|
||||
}
|
||||
return history?.fastest;
|
||||
}
|
||||
|
||||
PathRecord? getMostRecentPath(String contactPubKeyHex) {
|
||||
final history = _cache[contactPubKeyHex];
|
||||
if (history != null) {
|
||||
_trackAccess(contactPubKeyHex);
|
||||
}
|
||||
return history?.mostRecent;
|
||||
}
|
||||
|
||||
Future<void> clearPathHistory(String contactPubKeyHex) async {
|
||||
_cache.remove(contactPubKeyHex);
|
||||
_cacheAccessOrder.remove(contactPubKeyHex);
|
||||
_autoRotationIndex.remove(contactPubKeyHex);
|
||||
_floodStats.remove(contactPubKeyHex);
|
||||
await _storage.clearPathHistory(contactPubKeyHex);
|
||||
@@ -314,6 +332,20 @@ class PathHistoryService extends ChangeNotifier {
|
||||
final stats = _floodStats.putIfAbsent(contactPubKeyHex, () => _FloodStats());
|
||||
stats.lastUsed = DateTime.now();
|
||||
}
|
||||
|
||||
void _trackAccess(String contactPubKeyHex) {
|
||||
_cacheAccessOrder.remove(contactPubKeyHex);
|
||||
_cacheAccessOrder.add(contactPubKeyHex);
|
||||
}
|
||||
|
||||
void _evictIfNeeded() {
|
||||
while (_cache.length > _maxCachedContacts && _cacheAccessOrder.isNotEmpty) {
|
||||
final oldest = _cacheAccessOrder.removeAt(0);
|
||||
_cache.remove(oldest);
|
||||
_autoRotationIndex.remove(oldest);
|
||||
_floodStats.remove(oldest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _FloodStats {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../storage/prefs_manager.dart';
|
||||
|
||||
class StorageService {
|
||||
static const String _pathHistoryPrefix = 'path_history_';
|
||||
@@ -9,14 +9,14 @@ class StorageService {
|
||||
|
||||
Future<void> savePathHistory(
|
||||
String contactPubKeyHex, ContactPathHistory history) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$_pathHistoryPrefix$contactPubKeyHex';
|
||||
final jsonStr = jsonEncode(history.toJson());
|
||||
await prefs.setString(key, jsonStr);
|
||||
}
|
||||
|
||||
Future<ContactPathHistory?> loadPathHistory(String contactPubKeyHex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$_pathHistoryPrefix$contactPubKeyHex';
|
||||
final jsonStr = prefs.getString(key);
|
||||
|
||||
@@ -31,13 +31,13 @@ class StorageService {
|
||||
}
|
||||
|
||||
Future<void> clearPathHistory(String contactPubKeyHex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final key = '$_pathHistoryPrefix$contactPubKeyHex';
|
||||
await prefs.remove(key);
|
||||
}
|
||||
|
||||
Future<void> clearAllPathHistories() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final keys = prefs.getKeys();
|
||||
final pathHistoryKeys =
|
||||
keys.where((key) => key.startsWith(_pathHistoryPrefix));
|
||||
@@ -48,7 +48,7 @@ class StorageService {
|
||||
}
|
||||
|
||||
Future<Map<String, String>> loadPendingMessages() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_pendingMessagesKey);
|
||||
|
||||
if (jsonStr == null) return {};
|
||||
@@ -62,20 +62,20 @@ class StorageService {
|
||||
}
|
||||
|
||||
Future<void> savePendingMessages(Map<String, String> pending) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = jsonEncode(pending);
|
||||
await prefs.setString(_pendingMessagesKey, jsonStr);
|
||||
}
|
||||
|
||||
Future<void> clearPendingMessages() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
await prefs.remove(_pendingMessagesKey);
|
||||
}
|
||||
|
||||
/// Save a repeater password by public key hex
|
||||
Future<void> saveRepeaterPassword(
|
||||
String repeaterPubKeyHex, String password) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final passwords = await loadRepeaterPasswords();
|
||||
passwords[repeaterPubKeyHex] = password;
|
||||
final jsonStr = jsonEncode(passwords);
|
||||
@@ -84,7 +84,7 @@ class StorageService {
|
||||
|
||||
/// Load all saved repeater passwords (map of pubKeyHex -> password)
|
||||
Future<Map<String, String>> loadRepeaterPasswords() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final jsonStr = prefs.getString(_repeaterPasswordsKey);
|
||||
|
||||
if (jsonStr == null) return {};
|
||||
@@ -105,7 +105,7 @@ class StorageService {
|
||||
|
||||
/// Remove a saved repeater password
|
||||
Future<void> removeRepeaterPassword(String repeaterPubKeyHex) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
final passwords = await loadRepeaterPasswords();
|
||||
passwords.remove(repeaterPubKeyHex);
|
||||
final jsonStr = jsonEncode(passwords);
|
||||
@@ -114,7 +114,7 @@ class StorageService {
|
||||
|
||||
/// Clear all saved repeater passwords
|
||||
Future<void> clearAllRepeaterPasswords() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final prefs = PrefsManager.instance;
|
||||
await prefs.remove(_repeaterPasswordsKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'codec2_ffi.dart';
|
||||
|
||||
class VoiceMessageService {
|
||||
static const int sampleRate = 8000;
|
||||
static const int channels = 1;
|
||||
static const int bitsPerSample = 16;
|
||||
static const int maxRecordSeconds = 5;
|
||||
static const int chunkRawBytes = 90;
|
||||
static const String codecName = 'codec2_1300';
|
||||
static const String chunkPrefix = 'V1|';
|
||||
|
||||
static final VoiceMessageService instance = VoiceMessageService._();
|
||||
|
||||
VoiceMessageService._();
|
||||
|
||||
Future<Directory> ensureVoiceDir() async {
|
||||
final docs = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory(path.join(docs.path, 'voice'));
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
String buildVoiceFileName({
|
||||
required String senderKeyHex,
|
||||
required int timestampSeconds,
|
||||
bool outgoing = false,
|
||||
}) {
|
||||
final suffix = outgoing ? 'out' : 'in';
|
||||
return 'voice_${senderKeyHex}_${timestampSeconds}_$suffix.wav';
|
||||
}
|
||||
|
||||
List<String> buildVoiceChunks(Uint8List codec2Bytes) {
|
||||
if (codec2Bytes.isEmpty) return [];
|
||||
final chunks = <Uint8List>[];
|
||||
for (var offset = 0; offset < codec2Bytes.length; offset += chunkRawBytes) {
|
||||
final end = (offset + chunkRawBytes).clamp(0, codec2Bytes.length).toInt();
|
||||
chunks.add(Uint8List.fromList(codec2Bytes.sublist(offset, end)));
|
||||
}
|
||||
final count = chunks.length;
|
||||
return List<String>.generate(count, (index) {
|
||||
final encoded = _base64UrlEncodeNoPad(chunks[index]);
|
||||
return '$chunkPrefix$index/$count|$encoded';
|
||||
});
|
||||
}
|
||||
|
||||
VoiceChunk? tryParseChunk(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (!trimmed.startsWith(chunkPrefix)) return null;
|
||||
final match = RegExp(r'^V1\|(\d+)/(\d+)\|([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||
if (match == null) return null;
|
||||
final idx = int.tryParse(match.group(1) ?? '');
|
||||
final count = int.tryParse(match.group(2) ?? '');
|
||||
final payload = match.group(3);
|
||||
if (idx == null || count == null || payload == null) return null;
|
||||
if (idx < 0 || count <= 0 || idx >= count) return null;
|
||||
try {
|
||||
final bytes = _base64UrlDecode(payload);
|
||||
return VoiceChunk(index: idx, count: count, bytes: bytes);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List encodePcmToCodec2(Uint8List pcmBytes) {
|
||||
final session = Codec2Ffi.instance.createSession();
|
||||
try {
|
||||
final samplesPerFrame = session.samplesPerFrameValue;
|
||||
final pcmSamples = _toInt16(pcmBytes);
|
||||
final frameCount = (pcmSamples.length + samplesPerFrame - 1) ~/ samplesPerFrame;
|
||||
final builder = BytesBuilder(copy: false);
|
||||
|
||||
for (var frameIndex = 0; frameIndex < frameCount; frameIndex++) {
|
||||
final start = frameIndex * samplesPerFrame;
|
||||
final end = (start + samplesPerFrame).clamp(0, pcmSamples.length).toInt();
|
||||
final frame = Int16List(samplesPerFrame);
|
||||
final copyLen = end - start;
|
||||
if (copyLen > 0) {
|
||||
frame.setRange(0, copyLen, pcmSamples.sublist(start, end));
|
||||
}
|
||||
final encoded = session.encodePcmFrame(frame);
|
||||
builder.add(encoded);
|
||||
}
|
||||
|
||||
return builder.takeBytes();
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List decodeCodec2ToPcm(Uint8List codec2Bytes) {
|
||||
final session = Codec2Ffi.instance.createSession();
|
||||
try {
|
||||
final bytesPerFrame = session.bytesPerFrameValue;
|
||||
if (bytesPerFrame <= 0) return Uint8List(0);
|
||||
final frameCount = codec2Bytes.length ~/ bytesPerFrame;
|
||||
final builder = BytesBuilder(copy: false);
|
||||
|
||||
for (var frameIndex = 0; frameIndex < frameCount; frameIndex++) {
|
||||
final start = frameIndex * bytesPerFrame;
|
||||
final frameBytes = codec2Bytes.sublist(start, start + bytesPerFrame);
|
||||
final decoded = session.decodeCodecFrame(frameBytes);
|
||||
builder.add(Uint8List.view(
|
||||
decoded.buffer,
|
||||
decoded.offsetInBytes,
|
||||
decoded.lengthInBytes,
|
||||
));
|
||||
}
|
||||
|
||||
return builder.takeBytes();
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
int durationMsForCodec2Bytes(Uint8List codec2Bytes) {
|
||||
final session = Codec2Ffi.instance.createSession();
|
||||
try {
|
||||
final bytesPerFrame = session.bytesPerFrameValue;
|
||||
final samplesPerFrame = session.samplesPerFrameValue;
|
||||
if (bytesPerFrame <= 0 || samplesPerFrame <= 0) return 0;
|
||||
final frameCount = codec2Bytes.length ~/ bytesPerFrame;
|
||||
final frameDurationMs = (samplesPerFrame * 1000 / sampleRate).round();
|
||||
return frameCount * frameDurationMs;
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> writeWavFile({
|
||||
required Uint8List pcmBytes,
|
||||
required String fileName,
|
||||
}) async {
|
||||
final dir = await ensureVoiceDir();
|
||||
final filePath = path.join(dir.path, fileName);
|
||||
final wavHeader = _buildWavHeader(
|
||||
pcmDataSize: pcmBytes.length,
|
||||
sampleRate: sampleRate,
|
||||
channels: channels,
|
||||
bitsPerSample: bitsPerSample,
|
||||
);
|
||||
final file = File(filePath);
|
||||
final builder = BytesBuilder(copy: false);
|
||||
builder.add(wavHeader);
|
||||
builder.add(pcmBytes);
|
||||
await file.writeAsBytes(builder.takeBytes(), flush: true);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
Uint8List _buildWavHeader({
|
||||
required int pcmDataSize,
|
||||
required int sampleRate,
|
||||
required int channels,
|
||||
required int bitsPerSample,
|
||||
}) {
|
||||
final byteRate = sampleRate * channels * (bitsPerSample ~/ 8);
|
||||
final blockAlign = channels * (bitsPerSample ~/ 8);
|
||||
final buffer = BytesBuilder(copy: false);
|
||||
buffer.add(ascii.encode('RIFF'));
|
||||
buffer.add(_le32(36 + pcmDataSize));
|
||||
buffer.add(ascii.encode('WAVE'));
|
||||
buffer.add(ascii.encode('fmt '));
|
||||
buffer.add(_le32(16));
|
||||
buffer.add(_le16(1));
|
||||
buffer.add(_le16(channels));
|
||||
buffer.add(_le32(sampleRate));
|
||||
buffer.add(_le32(byteRate));
|
||||
buffer.add(_le16(blockAlign));
|
||||
buffer.add(_le16(bitsPerSample));
|
||||
buffer.add(ascii.encode('data'));
|
||||
buffer.add(_le32(pcmDataSize));
|
||||
return buffer.takeBytes();
|
||||
}
|
||||
|
||||
Uint8List _le16(int value) {
|
||||
final data = ByteData(2)..setUint16(0, value, Endian.little);
|
||||
return data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Uint8List _le32(int value) {
|
||||
final data = ByteData(4)..setUint32(0, value, Endian.little);
|
||||
return data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Int16List _toInt16(Uint8List bytes) {
|
||||
final evenLength = bytes.lengthInBytes - (bytes.lengthInBytes % 2);
|
||||
if (evenLength <= 0) return Int16List(0);
|
||||
return Int16List.view(bytes.buffer, bytes.offsetInBytes, evenLength ~/ 2);
|
||||
}
|
||||
|
||||
String _base64UrlEncodeNoPad(Uint8List bytes) {
|
||||
return base64Url.encode(bytes).replaceAll('=', '');
|
||||
}
|
||||
|
||||
Uint8List _base64UrlDecode(String encoded) {
|
||||
final paddedLength = (encoded.length + 3) ~/ 4 * 4;
|
||||
final padded = encoded.padRight(paddedLength, '=');
|
||||
return base64Url.decode(padded);
|
||||
}
|
||||
}
|
||||
|
||||
class VoiceChunk {
|
||||
final int index;
|
||||
final int count;
|
||||
final Uint8List bytes;
|
||||
|
||||
VoiceChunk({
|
||||
required this.index,
|
||||
required this.count,
|
||||
required this.bytes,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user