mirror of
https://github.com/Senape3000/EvilCrowRF-V2.git
synced 2026-08-27 21:10:08 +00:00
Merge pull request #5 from Senape3000/Waterfall_dev
This commit is contained in:
+5
-1
@@ -10,4 +10,8 @@ releases/
|
||||
personal_docs/
|
||||
*.7z
|
||||
to-do/
|
||||
docs/session
|
||||
docs/session
|
||||
SDR/build
|
||||
SDR/dist
|
||||
SDR/upx
|
||||
SDR/sdr_launcher.spec
|
||||
+161
-1
@@ -34,6 +34,7 @@ public:
|
||||
handler.registerCommand(0x0F, handleMoveFile);
|
||||
handler.registerCommand(0x10, handleSaveToSignalsWithName);
|
||||
handler.registerCommand(0x14, handleGetDirectoryTree); // Changed from 0x12 to avoid conflict with startJam
|
||||
handler.registerCommand(0x18, handleFormatSDCard);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -747,6 +748,60 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Recursively remove a directory and all its contents.
|
||||
*
|
||||
* Walks the directory tree depth-first: deletes every file, recurses
|
||||
* into sub-directories, then removes the now-empty directory itself.
|
||||
*
|
||||
* @param fs Filesystem reference (SD or LittleFS).
|
||||
* @param path Absolute path of the directory to remove.
|
||||
* @return true if the directory and all children were deleted.
|
||||
*/
|
||||
static bool removeDirectoryRecursive(fs::FS& fs, const char* path) {
|
||||
File dir = fs.open(path);
|
||||
if (!dir || !dir.isDirectory()) {
|
||||
dir.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allOk = true;
|
||||
File child = dir.openNextFile();
|
||||
while (child) {
|
||||
// Copy path before close — child.path() is an internal pointer
|
||||
// that becomes invalid after child.close().
|
||||
char childPathBuf[256];
|
||||
strncpy(childPathBuf, child.path(), sizeof(childPathBuf) - 1);
|
||||
childPathBuf[sizeof(childPathBuf) - 1] = '\0';
|
||||
bool isDir = child.isDirectory();
|
||||
child.close();
|
||||
|
||||
if (isDir) {
|
||||
if (!removeDirectoryRecursive(fs, childPathBuf)) {
|
||||
ESP_LOGE("FileCmd", "Failed to remove dir: %s", childPathBuf);
|
||||
allOk = false;
|
||||
// Continue deleting other entries instead of aborting
|
||||
}
|
||||
} else {
|
||||
if (!fs.remove(childPathBuf)) {
|
||||
ESP_LOGE("FileCmd", "Failed to remove file: %s", childPathBuf);
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
// Yield to prevent watchdog timeout on deep/large trees
|
||||
vTaskDelay(1);
|
||||
child = dir.openNextFile();
|
||||
}
|
||||
dir.close();
|
||||
|
||||
// Directory should now be empty — remove it
|
||||
if (!fs.rmdir(path)) {
|
||||
ESP_LOGE("FileCmd", "Failed to rmdir: %s", path);
|
||||
return false;
|
||||
}
|
||||
return allOk;
|
||||
}
|
||||
|
||||
static bool handleRemoveFile(const uint8_t* data, size_t len) {
|
||||
if (len < 2) {
|
||||
sendBinaryFileActionResult(1, false, 1); // 1=delete, error 1=insufficient data
|
||||
@@ -778,7 +833,7 @@ public:
|
||||
|
||||
bool ok = false;
|
||||
if (isDirectory) {
|
||||
ok = fs.rmdir(pathBuffer.c_str());
|
||||
ok = removeDirectoryRecursive(fs, pathBuffer.c_str());
|
||||
} else {
|
||||
ok = fs.remove(pathBuffer.c_str());
|
||||
}
|
||||
@@ -786,6 +841,111 @@ public:
|
||||
sendBinaryFileActionResult(1, ok, ok ? 0 : 4, pathBuffer.c_str()); // error 4=delete failed
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Format SD card: recursively delete all contents and re-create
|
||||
* the default directory structure.
|
||||
* Sends progressive feedback (errorCode 0xFF = in-progress step).
|
||||
*
|
||||
* Payload: [0x46][0x53] ('FS') as confirmation guard — prevents
|
||||
* accidental invocation.
|
||||
*/
|
||||
static bool handleFormatSDCard(const uint8_t* data, size_t len) {
|
||||
// Require 2-byte confirmation payload 'FS' (Format SD)
|
||||
if (len < 2 || data[0] != 0x46 || data[1] != 0x53) {
|
||||
ESP_LOGW("FileCmd", "Format SD rejected: missing confirmation 'FS'");
|
||||
sendBinaryFileActionResult(8, false, 1); // actionType 8 = format
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGW("FileCmd", "FORMAT SD CARD — deleting all contents");
|
||||
|
||||
// Phase 1: notify app that format has started
|
||||
sendBinaryFileActionResult(8, true, 0xFF, "Starting format...");
|
||||
vTaskDelay(pdMS_TO_TICKS(50)); // Let BLE send the notification
|
||||
|
||||
// Phase 2: recursively delete every entry in SD root
|
||||
File root = SD.open("/");
|
||||
if (!root || !root.isDirectory()) {
|
||||
ESP_LOGE("FileCmd", "Cannot open SD root");
|
||||
sendBinaryFileActionResult(8, false, 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allOk = true;
|
||||
int deletedCount = 0;
|
||||
File child = root.openNextFile();
|
||||
while (child) {
|
||||
// Copy path to local buffer before close — child.path()
|
||||
// returns an internal pointer invalidated by close().
|
||||
char childPathBuf[256];
|
||||
strncpy(childPathBuf, child.path(), sizeof(childPathBuf) - 1);
|
||||
childPathBuf[sizeof(childPathBuf) - 1] = '\0';
|
||||
bool isDir = child.isDirectory();
|
||||
child.close();
|
||||
|
||||
// Send progress notification for each item being deleted
|
||||
char progressMsg[280];
|
||||
snprintf(progressMsg, sizeof(progressMsg), "Deleting: %s", childPathBuf);
|
||||
sendBinaryFileActionResult(8, true, 0xFF, progressMsg);
|
||||
vTaskDelay(pdMS_TO_TICKS(20)); // Let BLE send + prevent WDT
|
||||
|
||||
if (isDir) {
|
||||
if (!removeDirectoryRecursive(SD, childPathBuf)) {
|
||||
ESP_LOGE("FileCmd", "Failed to remove dir: %s", childPathBuf);
|
||||
allOk = false;
|
||||
}
|
||||
} else {
|
||||
if (!SD.remove(childPathBuf)) {
|
||||
ESP_LOGE("FileCmd", "Failed to remove file: %s", childPathBuf);
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
deletedCount++;
|
||||
// Yield to prevent watchdog timeout during format
|
||||
vTaskDelay(1);
|
||||
child = root.openNextFile();
|
||||
}
|
||||
root.close();
|
||||
|
||||
ESP_LOGI("FileCmd", "Deleted %d items from SD root", deletedCount);
|
||||
|
||||
// Phase 3: re-create default directory structure with progress and verification
|
||||
static const char* defaultDirs[] = {
|
||||
"/DATA",
|
||||
"/DATA/RECORDS",
|
||||
"/DATA/SIGNALS",
|
||||
"/DATA/PRESETS",
|
||||
"/DATA/TEMP"
|
||||
};
|
||||
bool creationSuccess = true;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
char progressMsg[280];
|
||||
snprintf(progressMsg, sizeof(progressMsg), "Creating: %s", defaultDirs[i]);
|
||||
sendBinaryFileActionResult(8, true, 0xFF, progressMsg);
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
|
||||
// Create directory and verify
|
||||
if (!SD.mkdir(defaultDirs[i])) {
|
||||
// mkdir returns false if directory already exists or creation failed
|
||||
// Check if it exists to distinguish between these cases
|
||||
if (!SD.exists(defaultDirs[i])) {
|
||||
ESP_LOGE("FileCmd", "Failed to create directory: %s", defaultDirs[i]);
|
||||
creationSuccess = false;
|
||||
allOk = false;
|
||||
} else {
|
||||
ESP_LOGI("FileCmd", "Directory already exists: %s", defaultDirs[i]);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI("FileCmd", "Created directory: %s", defaultDirs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI("FileCmd", "SD card format %s", allOk ? "complete" : "completed with errors");
|
||||
// Send final result (errorCode 0 = done successfully, 4 = done with errors)
|
||||
sendBinaryFileActionResult(8, allOk, allOk ? 0 : 4);
|
||||
return allOk;
|
||||
}
|
||||
|
||||
static bool handleRenameFile(const uint8_t* data, size_t len) {
|
||||
if (len < 3) {
|
||||
|
||||
+3
-3
@@ -7,9 +7,9 @@
|
||||
// - PATCH: Bug fixes, optimizations
|
||||
// The app will compare these values for FW update matching.
|
||||
#define FIRMWARE_VERSION_MAJOR 1
|
||||
#define FIRMWARE_VERSION_MINOR 0
|
||||
#define FIRMWARE_VERSION_PATCH 3
|
||||
#define FIRMWARE_VERSION_STRING "1.0.3"
|
||||
#define FIRMWARE_VERSION_MINOR 1
|
||||
#define FIRMWARE_VERSION_PATCH 0
|
||||
#define FIRMWARE_VERSION_STRING "1.1.0"
|
||||
|
||||
#define CC1101_NUM_MODULES 2
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// This is a generated file; do not edit or check into version control.
|
||||
FLUTTER_ROOT=C:\Flutter\flutter
|
||||
FLUTTER_APPLICATION_PATH=C:\Users\Andrea\VSCode_project\EvilCrow-V2\mobile_app
|
||||
FLUTTER_APPLICATION_PATH=C:\Users\Andrea\VSCode_project\EvilCrow-RF-V2\EvilCrowRF-V2\mobile_app
|
||||
COCOAPODS_PARALLEL_CODE_SIGN=true
|
||||
FLUTTER_TARGET=lib\main.dart
|
||||
FLUTTER_BUILD_DIR=build
|
||||
FLUTTER_BUILD_NAME=1.1.0
|
||||
FLUTTER_BUILD_NUMBER=6
|
||||
FLUTTER_BUILD_NAME=1.0.3
|
||||
FLUTTER_BUILD_NUMBER=16
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
|
||||
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
|
||||
DART_OBFUSCATION=false
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/bin/sh
|
||||
# This is a generated file; do not edit or check into version control.
|
||||
export "FLUTTER_ROOT=C:\Flutter\flutter"
|
||||
export "FLUTTER_APPLICATION_PATH=C:\Users\Andrea\VSCode_project\EvilCrow-V2\mobile_app"
|
||||
export "FLUTTER_APPLICATION_PATH=C:\Users\Andrea\VSCode_project\EvilCrow-RF-V2\EvilCrowRF-V2\mobile_app"
|
||||
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||
export "FLUTTER_TARGET=lib\main.dart"
|
||||
export "FLUTTER_BUILD_DIR=build"
|
||||
export "FLUTTER_BUILD_NAME=1.1.0"
|
||||
export "FLUTTER_BUILD_NUMBER=6"
|
||||
export "FLUTTER_BUILD_NAME=1.0.3"
|
||||
export "FLUTTER_BUILD_NUMBER=16"
|
||||
export "DART_OBFUSCATION=false"
|
||||
export "TRACK_WIDGET_CREATION=true"
|
||||
export "TREE_SHAKE_ICONS=false"
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
}
|
||||
},
|
||||
"unknownDevice": "Unknown Device",
|
||||
"noSupportedDevicesShowAll": "No supported devices found. Select your device manually:",
|
||||
|
||||
"notConnectedToDevice": "Not connected to device",
|
||||
"connectToDeviceToManageFiles": "Connect to a device to manage files",
|
||||
@@ -1054,6 +1055,7 @@
|
||||
"appSettings": "App Settings",
|
||||
"appSettingsSubtitle": "Language, cache, permissions",
|
||||
"rfSettings": "RF Settings",
|
||||
"rfSettingsSubtitle": "Bruteforce, Radio & Scanner settings",
|
||||
"syncedWithDevice": "Synced with device",
|
||||
"localOnly": "Local only",
|
||||
"bruteforceSettings": "Bruteforce Settings",
|
||||
|
||||
@@ -362,6 +362,12 @@ abstract class AppLocalizations {
|
||||
/// **'Unknown Device'**
|
||||
String get unknownDevice;
|
||||
|
||||
/// No description provided for @noSupportedDevicesShowAll.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No supported devices found. Select your device manually:'**
|
||||
String get noSupportedDevicesShowAll;
|
||||
|
||||
/// No description provided for @notConnectedToDevice.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2654,6 +2660,12 @@ abstract class AppLocalizations {
|
||||
/// **'RF Settings'**
|
||||
String get rfSettings;
|
||||
|
||||
/// No description provided for @rfSettingsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bruteforce, Radio & Scanner settings'**
|
||||
String get rfSettingsSubtitle;
|
||||
|
||||
/// No description provided for @syncedWithDevice.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -165,6 +165,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get unknownDevice => 'Unknown Device';
|
||||
|
||||
@override
|
||||
String get noSupportedDevicesShowAll =>
|
||||
'No supported devices found. Select your device manually:';
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Not connected to device';
|
||||
|
||||
@@ -1492,6 +1496,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get rfSettings => 'RF Settings';
|
||||
|
||||
@override
|
||||
String get rfSettingsSubtitle => 'Bruteforce, Radio & Scanner settings';
|
||||
|
||||
@override
|
||||
String get syncedWithDevice => 'Synced with device';
|
||||
|
||||
|
||||
@@ -167,6 +167,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get unknownDevice => 'Неизвестное устройство';
|
||||
|
||||
@override
|
||||
String get noSupportedDevicesShowAll =>
|
||||
'Устройства не распознаны. Выберите вручную:';
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Не подключено к устройству';
|
||||
|
||||
@@ -1499,6 +1503,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get rfSettings => 'Настройки RF';
|
||||
|
||||
@override
|
||||
String get rfSettingsSubtitle => 'Брутфорс, радио и сканер';
|
||||
|
||||
@override
|
||||
String get syncedWithDevice => 'Синхронизировано с устройством';
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
}
|
||||
},
|
||||
"unknownDevice": "Неизвестное устройство",
|
||||
"noSupportedDevicesShowAll": "Устройства не распознаны. Выберите вручную:",
|
||||
|
||||
"notConnectedToDevice": "Не подключено к устройству",
|
||||
"connectToDeviceToManageFiles": "Подключитесь к устройству для управления файлами",
|
||||
@@ -1040,6 +1041,7 @@
|
||||
"appSettings": "Настройки приложения",
|
||||
"appSettingsSubtitle": "Язык, кэш, разрешения",
|
||||
"rfSettings": "Настройки RF",
|
||||
"rfSettingsSubtitle": "Брутфорс, радио и сканер",
|
||||
"syncedWithDevice": "Синхронизировано с устройством",
|
||||
"localOnly": "Только локально",
|
||||
"bruteforceSettings": "Настройки брутфорса",
|
||||
|
||||
@@ -104,7 +104,10 @@ class BleProvider extends ChangeNotifier {
|
||||
int currentPathType = 5; // 0=/DATA/RECORDS, 1=/DATA/SIGNALS, 2=/DATA/PRESETS, 3=/DATA/TEMP, 4=INTERNAL (LittleFS), 5=SD Root
|
||||
bool isLoadingFiles = false;
|
||||
double fileListProgress = 0.0; // Progress for file list loading (0.0 to 1.0)
|
||||
|
||||
bool isFormattingSD = false; // True while SD format command is in progress
|
||||
bool sdFormatSuccess = false; // Result of last SD format
|
||||
String sdFormatProgress = ''; // Progress message during SD format (e.g. "Deleting: /somefile")
|
||||
|
||||
// Scanner state
|
||||
List<DetectedSignal> detectedSignals = [];
|
||||
Map<String, double> frequencySpectrum = {};
|
||||
@@ -694,6 +697,7 @@ class BleProvider extends ChangeNotifier {
|
||||
_resetConnectionState();
|
||||
_log('info', 'Disconnected from device');
|
||||
isLoadingFiles = false;
|
||||
isFormattingSD = false;
|
||||
|
||||
// Clear cache on disconnect
|
||||
_fileCache.clear();
|
||||
@@ -1199,38 +1203,29 @@ class BleProvider extends ChangeNotifier {
|
||||
// Methods for working with files
|
||||
|
||||
/// Reads file content from ESP
|
||||
Future<String> readFileContent(String filePath, {String? basePath}) async {
|
||||
Future<String> readFileContent(String filePath, {int? pathType}) async {
|
||||
if (!isConnected) {
|
||||
throw Exception('Device not connected');
|
||||
}
|
||||
|
||||
// Determine pathType based on basePath
|
||||
int pathType = 0; // Default to /DATA/RECORDS
|
||||
String relativePath = filePath;
|
||||
// Use provided pathType or default to RECORDS (0)
|
||||
int effectivePathType = pathType ?? 0;
|
||||
|
||||
if (basePath == '/DATA/SIGNALS') {
|
||||
pathType = 1;
|
||||
} else if (basePath == '/DATA/PRESETS') {
|
||||
pathType = 2;
|
||||
} else if (basePath == '/DATA/TEMP') {
|
||||
pathType = 3;
|
||||
} else if (basePath == '/SDROOT') {
|
||||
pathType = 5;
|
||||
} else if (basePath == '/') {
|
||||
pathType = 4; // LittleFS internal storage
|
||||
// For pathType 0-3 (RECORDS, SIGNALS, PRESETS, TEMP), extract relative path
|
||||
// because firmware adds /DATA/XXXX/ prefix automatically.
|
||||
// For pathType 4-5 (LittleFS root, SD root), keep full absolute path.
|
||||
String pathToUse = filePath;
|
||||
if (effectivePathType >= 0 && effectivePathType < 4 && filePath.startsWith('/DATA/')) {
|
||||
// Extract relative path from /DATA/RECORDS/... or /DATA/SIGNALS/...
|
||||
final parts = filePath.split('/');
|
||||
if (parts.length > 3) {
|
||||
pathToUse = parts.sublist(3).join('/');
|
||||
} else {
|
||||
pathToUse = parts.last;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove /DATA/ prefix if present (shouldn't be, but handle it)
|
||||
if (relativePath.startsWith('/DATA/')) {
|
||||
relativePath = relativePath.substring(6); // Remove '/DATA/'
|
||||
}
|
||||
|
||||
// Remove leading slash if present
|
||||
if (relativePath.startsWith('/')) {
|
||||
relativePath = relativePath.substring(1);
|
||||
}
|
||||
|
||||
_log('INFO', 'Reading file content: $relativePath (pathType: $pathType, basePath: $basePath)');
|
||||
_log('INFO', 'Reading file content: $pathToUse (pathType: $effectivePathType)');
|
||||
|
||||
// Set loading flag
|
||||
isLoadingFileContent = true;
|
||||
@@ -1243,9 +1238,9 @@ class BleProvider extends ChangeNotifier {
|
||||
final completer = Completer<String>();
|
||||
_pendingFileReadCompleter = completer;
|
||||
|
||||
// Use binary command with path type - pass full relative path including subdirectory
|
||||
final command = FirmwareBinaryProtocol.createLoadFileDataCommand(relativePath, pathType: pathType);
|
||||
_log('INFO', 'Sending binary command for file: $relativePath (pathType: $pathType, command length: ${command.length})');
|
||||
// Use binary command with path type
|
||||
final command = FirmwareBinaryProtocol.createLoadFileDataCommand(pathToUse, pathType: effectivePathType);
|
||||
_log('INFO', 'Sending binary command for file: $pathToUse (pathType: $effectivePathType, command length: ${command.length})');
|
||||
|
||||
// Send binary file read command
|
||||
await sendBinaryCommand(command);
|
||||
@@ -1287,10 +1282,8 @@ class BleProvider extends ChangeNotifier {
|
||||
_log('INFO', 'Downloading file: $filePath');
|
||||
|
||||
// Using readFileContent, which already uses binary protocol
|
||||
// Determine pathType based on current path
|
||||
try {
|
||||
final basePath = _getBasePathForPathType(currentPathType);
|
||||
final content = await readFileContent(filePath, basePath: basePath);
|
||||
final content = await readFileContent(filePath, pathType: currentPathType);
|
||||
|
||||
// Call progress callback if available
|
||||
onProgress?.call(1.0);
|
||||
@@ -2545,6 +2538,26 @@ class BleProvider extends ChangeNotifier {
|
||||
_handleFileUploadResponse(responseData);
|
||||
}
|
||||
|
||||
// Handle format-sd response
|
||||
if (responseData.containsKey('action') && responseData['action'] == 'format-sd') {
|
||||
print('Format SD response received: $responseData');
|
||||
|
||||
// Check if this is a progress update (errorCode 0xFF) or final result
|
||||
if (responseData['isProgress'] == true) {
|
||||
// Progress update — update message but keep formatting state
|
||||
sdFormatProgress = responseData['progressMessage']?.toString() ?? '';
|
||||
_log('info', 'SD format progress: $sdFormatProgress');
|
||||
notifyListeners();
|
||||
} else {
|
||||
// Final result
|
||||
isFormattingSD = false;
|
||||
sdFormatProgress = '';
|
||||
sdFormatSuccess = responseData['success'] == true;
|
||||
_log('info', 'SD format ${sdFormatSuccess ? 'succeeded' : 'failed'}');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle copy response
|
||||
if (responseData.containsKey('action') && responseData['action'] == 'copy') {
|
||||
print('Copy response received: $responseData');
|
||||
@@ -2998,10 +3011,11 @@ class BleProvider extends ChangeNotifier {
|
||||
// Use provided pathType or current
|
||||
int effectivePathType = pathType ?? currentPathType;
|
||||
|
||||
// Use the filePath as-is (it should be a relative path like "folder/file.sub" or just "file.sub")
|
||||
// Only extract filename if it's an absolute path starting with /DATA/
|
||||
// For pathType 0-3 (RECORDS, SIGNALS, PRESETS, TEMP), extract relative path
|
||||
// because firmware adds /DATA/XXXX/ prefix automatically.
|
||||
// For pathType 4-5 (LittleFS root, SD root), keep full absolute path.
|
||||
String pathToUse = filePath;
|
||||
if (filePath.startsWith('/DATA/')) {
|
||||
if (effectivePathType >= 0 && effectivePathType < 4 && filePath.startsWith('/DATA/')) {
|
||||
// Extract relative path from /DATA/RECORDS/... or /DATA/SIGNALS/...
|
||||
final parts = filePath.split('/');
|
||||
if (parts.length > 3) {
|
||||
@@ -3233,6 +3247,25 @@ class BleProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Format SD card: recursively delete all contents and re-create defaults.
|
||||
Future<bool> formatSDCard() async {
|
||||
if (!isConnected || txCharacteristic == null) return false;
|
||||
try {
|
||||
final cmd = FirmwareBinaryProtocol.createFormatSDCommand();
|
||||
isFormattingSD = true;
|
||||
sdFormatSuccess = false;
|
||||
notifyListeners();
|
||||
await sendBinaryCommand(cmd);
|
||||
_log('warning', 'Format SD command sent');
|
||||
return true;
|
||||
} catch (e) {
|
||||
isFormattingSD = false;
|
||||
notifyListeners();
|
||||
_log('error', 'Failed to send format SD: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a bruter attack with the given menu choice (1-33)
|
||||
Future<void> sendBruterCommand(int menuChoice) async {
|
||||
if (!isConnected || txCharacteristic == null) {
|
||||
@@ -3255,6 +3288,35 @@ class BleProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Start a custom De Bruijn attack with per-protocol timing and frequency.
|
||||
/// Uses firmware sub-command 0xFD to pass exact Te, ratio, bits, and
|
||||
/// frequency instead of relying on hardcoded De Bruijn menus.
|
||||
Future<void> sendCustomDeBruijnCommand({
|
||||
required int bits,
|
||||
required int te,
|
||||
required int ratio,
|
||||
required double frequencyMhz,
|
||||
}) async {
|
||||
if (!isConnected || txCharacteristic == null) {
|
||||
_log('error', 'Cannot start custom De Bruijn: not connected');
|
||||
throw Exception('Not connected');
|
||||
}
|
||||
|
||||
_log('command', 'Starting custom De Bruijn: bits=$bits te=$te ratio=$ratio freq=$frequencyMhz');
|
||||
|
||||
final command = FirmwareBinaryProtocol.createCustomDeBruijnCommand(
|
||||
bits: bits,
|
||||
te: te,
|
||||
ratio: ratio,
|
||||
frequencyMhz: frequencyMhz,
|
||||
);
|
||||
await sendBinaryCommand(command);
|
||||
|
||||
_isBruterRunning = true;
|
||||
_bruterActiveProtocol = 0xFD;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Cancel a running bruter attack (STOP — clears saved state)
|
||||
Future<void> sendBruterCancelCommand() async {
|
||||
if (!isConnected || txCharacteristic == null) {
|
||||
|
||||
@@ -69,6 +69,7 @@ class FirmwareBinaryProtocol {
|
||||
static const int MSG_REBOOT = 0x15;
|
||||
static const int MSG_FACTORY_RESET = 0x16;
|
||||
static const int MSG_SET_DEVICE_NAME = 0x17;
|
||||
static const int MSG_FORMAT_SD = 0x18;
|
||||
|
||||
// Bruter command
|
||||
static const int MSG_BRUTER = 0x04;
|
||||
@@ -820,6 +821,31 @@ class FirmwareBinaryProtocol {
|
||||
return _createEnhancedCommand(MSG_BRUTER, payload);
|
||||
}
|
||||
|
||||
/// Create custom De Bruijn command (sub-command 0xFD) with per-protocol params.
|
||||
/// Format: [0xFD][bits:1][teLo:1][teHi:1][ratio:1][freq:4LE float] (9 bytes)
|
||||
/// This sends the correct timing and frequency for any protocol, avoiding
|
||||
/// hardcoded De Bruijn menus (35-39) which use fixed frequencies.
|
||||
static Uint8List createCustomDeBruijnCommand({
|
||||
required int bits,
|
||||
required int te,
|
||||
required int ratio,
|
||||
required double frequencyMhz,
|
||||
}) {
|
||||
final payload = Uint8List(9);
|
||||
payload[0] = 0xFD; // Sub-command: custom De Bruijn
|
||||
payload[1] = bits & 0xFF;
|
||||
payload[2] = te & 0xFF; // Te low byte
|
||||
payload[3] = (te >> 8) & 0xFF; // Te high byte
|
||||
payload[4] = ratio & 0xFF;
|
||||
// IEEE 754 float, little-endian
|
||||
final freqBytes = ByteData(4)..setFloat32(0, frequencyMhz, Endian.little);
|
||||
payload[5] = freqBytes.getUint8(0);
|
||||
payload[6] = freqBytes.getUint8(1);
|
||||
payload[7] = freqBytes.getUint8(2);
|
||||
payload[8] = freqBytes.getUint8(3);
|
||||
return _createEnhancedCommand(MSG_BRUTER, payload);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// NRF24 Command Factories (0x20-0x2E)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -1146,4 +1172,12 @@ class FirmwareBinaryProtocol {
|
||||
final payload = Uint8List.fromList([0x46, 0x52]); // 'F', 'R'
|
||||
return _createEnhancedCommand(MSG_FACTORY_RESET, payload);
|
||||
}
|
||||
|
||||
/// Format SD card (0x18)
|
||||
/// Payload: [0x46][0x53] ('FS') as safety confirmation.
|
||||
/// Recursively deletes all SD contents and re-creates default directories.
|
||||
static Uint8List createFormatSDCommand() {
|
||||
final payload = Uint8List.fromList([0x46, 0x53]); // 'F', 'S'
|
||||
return _createEnhancedCommand(MSG_FORMAT_SD, payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ class SettingsProvider with ChangeNotifier {
|
||||
int _nrfDataRate = 0; // 0=1MBPS, 1=2MBPS, 2=250KBPS
|
||||
int _nrfChannel = 76; // Default channel (0-125)
|
||||
int _nrfAutoRetransmit = 5; // Retransmit count (0-15)
|
||||
|
||||
bool get debugMode => _debugMode;
|
||||
int get bruterDelayMs => _bruterDelayMs;
|
||||
int get bruterModule => _bruterModule;
|
||||
|
||||
@@ -15,6 +15,10 @@ class BruterProtocol {
|
||||
final int bits;
|
||||
final String encoding;
|
||||
final IconData icon;
|
||||
/// Timing element in microseconds (shortest pulse duration)
|
||||
final int te;
|
||||
/// Ratio of long pulse to short pulse (e.g. 3 means 1:3)
|
||||
final int ratio;
|
||||
|
||||
const BruterProtocol({
|
||||
required this.menuId,
|
||||
@@ -24,6 +28,8 @@ class BruterProtocol {
|
||||
required this.bits,
|
||||
required this.encoding,
|
||||
required this.icon,
|
||||
this.te = 300,
|
||||
this.ratio = 3,
|
||||
});
|
||||
|
||||
/// Whether this protocol uses De Bruijn mode (menu 35-40)
|
||||
@@ -94,58 +100,58 @@ class BruterProtocol {
|
||||
/// All supported bruter protocols
|
||||
const List<BruterProtocol> bruterProtocols = [
|
||||
// EU Garage Remotes
|
||||
BruterProtocol(menuId: 1, name: 'CAME', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 2, name: 'Princeton', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'tristate', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 3, name: 'NiceFlo', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 6, name: 'Holtek', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 8, name: 'Ansonic', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 11, name: 'FAAC', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 12, name: 'BFT', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 13, name: 'SMC5326', category: 'EU Garage', frequencyMhz: 433.42, bits: 12, encoding: 'tristate', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 14, name: 'Clemsa', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 15, name: 'GateTX', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 16, name: 'Phox', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 17, name: 'Phoenix V2', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 18, name: 'Prastel', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 19, name: 'Doitrand', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage),
|
||||
BruterProtocol(menuId: 1, name: 'CAME', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 320, ratio: 2),
|
||||
BruterProtocol(menuId: 2, name: 'Princeton', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'tristate', icon: Icons.garage, te: 350, ratio: 3),
|
||||
BruterProtocol(menuId: 3, name: 'NiceFlo', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 700, ratio: 2),
|
||||
BruterProtocol(menuId: 6, name: 'Holtek', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 430, ratio: 2),
|
||||
BruterProtocol(menuId: 8, name: 'Ansonic', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 555, ratio: 2),
|
||||
BruterProtocol(menuId: 11, name: 'FAAC', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 400, ratio: 3),
|
||||
BruterProtocol(menuId: 12, name: 'BFT', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 400, ratio: 2),
|
||||
BruterProtocol(menuId: 13, name: 'SMC5326', category: 'EU Garage', frequencyMhz: 433.42, bits: 12, encoding: 'tristate', icon: Icons.garage, te: 320, ratio: 3),
|
||||
BruterProtocol(menuId: 14, name: 'Clemsa', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 400, ratio: 2),
|
||||
BruterProtocol(menuId: 15, name: 'GateTX', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 350, ratio: 2),
|
||||
BruterProtocol(menuId: 16, name: 'Phox', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 400, ratio: 2),
|
||||
BruterProtocol(menuId: 17, name: 'Phoenix V2', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 500, ratio: 2),
|
||||
BruterProtocol(menuId: 18, name: 'Prastel', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 400, ratio: 2),
|
||||
BruterProtocol(menuId: 19, name: 'Doitrand', category: 'EU Garage', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.garage, te: 400, ratio: 2),
|
||||
|
||||
// US Garage Remotes
|
||||
BruterProtocol(menuId: 4, name: 'Chamberlain', category: 'US Garage', frequencyMhz: 315.0, bits: 12, encoding: 'binary', icon: Icons.door_sliding),
|
||||
BruterProtocol(menuId: 5, name: 'Linear', category: 'US Garage', frequencyMhz: 300.0, bits: 10, encoding: 'binary', icon: Icons.door_sliding),
|
||||
BruterProtocol(menuId: 7, name: 'LiftMaster', category: 'US Garage', frequencyMhz: 315.0, bits: 12, encoding: 'binary', icon: Icons.door_sliding),
|
||||
BruterProtocol(menuId: 23, name: 'Firefly', category: 'US Garage', frequencyMhz: 300.0, bits: 10, encoding: 'binary', icon: Icons.door_sliding),
|
||||
BruterProtocol(menuId: 24, name: 'Linear MegaCode', category: 'US Garage', frequencyMhz: 318.0, bits: 24, encoding: 'binary', icon: Icons.door_sliding),
|
||||
BruterProtocol(menuId: 4, name: 'Chamberlain', category: 'US Garage', frequencyMhz: 315.0, bits: 12, encoding: 'binary', icon: Icons.door_sliding, te: 430, ratio: 2),
|
||||
BruterProtocol(menuId: 5, name: 'Linear', category: 'US Garage', frequencyMhz: 300.0, bits: 10, encoding: 'binary', icon: Icons.door_sliding, te: 500, ratio: 3),
|
||||
BruterProtocol(menuId: 7, name: 'LiftMaster', category: 'US Garage', frequencyMhz: 315.0, bits: 12, encoding: 'binary', icon: Icons.door_sliding, te: 400, ratio: 2),
|
||||
BruterProtocol(menuId: 23, name: 'Firefly', category: 'US Garage', frequencyMhz: 300.0, bits: 10, encoding: 'binary', icon: Icons.door_sliding, te: 400, ratio: 2),
|
||||
BruterProtocol(menuId: 24, name: 'Linear MegaCode', category: 'US Garage', frequencyMhz: 318.0, bits: 24, encoding: 'binary', icon: Icons.door_sliding, te: 500, ratio: 2),
|
||||
|
||||
// Home Automation
|
||||
BruterProtocol(menuId: 20, name: 'Dooya', category: 'Home Auto', frequencyMhz: 433.92, bits: 24, encoding: 'binary', icon: Icons.blinds),
|
||||
BruterProtocol(menuId: 21, name: 'Nero', category: 'Home Auto', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.blinds),
|
||||
BruterProtocol(menuId: 22, name: 'Magellen', category: 'Home Auto', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.blinds),
|
||||
BruterProtocol(menuId: 20, name: 'Dooya', category: 'Home Auto', frequencyMhz: 433.92, bits: 24, encoding: 'binary', icon: Icons.blinds, te: 350, ratio: 2),
|
||||
BruterProtocol(menuId: 21, name: 'Nero', category: 'Home Auto', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.blinds, te: 450, ratio: 2),
|
||||
BruterProtocol(menuId: 22, name: 'Magellen', category: 'Home Auto', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.blinds, te: 400, ratio: 2),
|
||||
|
||||
// Alarm / Sensors
|
||||
BruterProtocol(menuId: 9, name: 'EV1527', category: 'Alarm', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.security),
|
||||
BruterProtocol(menuId: 10, name: 'Honeywell', category: 'Alarm', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.security),
|
||||
BruterProtocol(menuId: 29, name: 'EV1527 24b', category: 'Alarm', frequencyMhz: 433.92, bits: 24, encoding: 'binary', icon: Icons.security),
|
||||
BruterProtocol(menuId: 9, name: 'EV1527', category: 'Alarm', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.security, te: 320, ratio: 3),
|
||||
BruterProtocol(menuId: 10, name: 'Honeywell', category: 'Alarm', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.security, te: 300, ratio: 2),
|
||||
BruterProtocol(menuId: 29, name: 'EV1527 24b', category: 'Alarm', frequencyMhz: 433.92, bits: 24, encoding: 'binary', icon: Icons.security, te: 320, ratio: 3),
|
||||
|
||||
// 868 MHz
|
||||
BruterProtocol(menuId: 25, name: 'Hörmann', category: '868 MHz', frequencyMhz: 868.35, bits: 12, encoding: 'binary', icon: Icons.radio),
|
||||
BruterProtocol(menuId: 26, name: 'Marantec', category: '868 MHz', frequencyMhz: 868.35, bits: 12, encoding: 'binary', icon: Icons.radio),
|
||||
BruterProtocol(menuId: 27, name: 'Berner', category: '868 MHz', frequencyMhz: 868.35, bits: 12, encoding: 'binary', icon: Icons.radio),
|
||||
BruterProtocol(menuId: 25, name: 'Hörmann', category: '868 MHz', frequencyMhz: 868.35, bits: 12, encoding: 'binary', icon: Icons.radio, te: 500, ratio: 2),
|
||||
BruterProtocol(menuId: 26, name: 'Marantec', category: '868 MHz', frequencyMhz: 868.35, bits: 12, encoding: 'binary', icon: Icons.radio, te: 600, ratio: 2),
|
||||
BruterProtocol(menuId: 27, name: 'Berner', category: '868 MHz', frequencyMhz: 868.35, bits: 12, encoding: 'binary', icon: Icons.radio, te: 400, ratio: 2),
|
||||
|
||||
// Misc
|
||||
BruterProtocol(menuId: 28, name: 'Intertechno V3', category: 'Misc', frequencyMhz: 433.92, bits: 32, encoding: 'binary', icon: Icons.power),
|
||||
BruterProtocol(menuId: 30, name: 'StarLine', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key),
|
||||
BruterProtocol(menuId: 31, name: 'Tedsen', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key),
|
||||
BruterProtocol(menuId: 32, name: 'Airforce', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key),
|
||||
BruterProtocol(menuId: 33, name: 'Unilarm', category: 'Misc', frequencyMhz: 433.42, bits: 12, encoding: 'binary', icon: Icons.key),
|
||||
BruterProtocol(menuId: 34, name: 'ELKA', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key),
|
||||
BruterProtocol(menuId: 28, name: 'Intertechno V3', category: 'Misc', frequencyMhz: 433.92, bits: 32, encoding: 'binary', icon: Icons.power, te: 250, ratio: 5),
|
||||
BruterProtocol(menuId: 30, name: 'StarLine', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key, te: 500, ratio: 2),
|
||||
BruterProtocol(menuId: 31, name: 'Tedsen', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key, te: 600, ratio: 2),
|
||||
BruterProtocol(menuId: 32, name: 'Airforce', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key, te: 350, ratio: 3),
|
||||
BruterProtocol(menuId: 33, name: 'Unilarm', category: 'Misc', frequencyMhz: 433.42, bits: 12, encoding: 'binary', icon: Icons.key, te: 350, ratio: 3),
|
||||
BruterProtocol(menuId: 34, name: 'ELKA', category: 'Misc', frequencyMhz: 433.92, bits: 12, encoding: 'binary', icon: Icons.key, te: 400, ratio: 2),
|
||||
|
||||
// De Bruijn protocols (~90x faster for binary ≤16 bits)
|
||||
BruterProtocol(menuId: 35, name: 'DeBruijn Generic 433', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.bolt),
|
||||
BruterProtocol(menuId: 36, name: 'DeBruijn Generic 315', category: 'De Bruijn', frequencyMhz: 315.0, bits: 12, encoding: 'debruijn', icon: Icons.bolt),
|
||||
BruterProtocol(menuId: 37, name: 'DeBruijn Holtek', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.bolt),
|
||||
BruterProtocol(menuId: 38, name: 'DeBruijn Linear', category: 'De Bruijn', frequencyMhz: 300.0, bits: 10, encoding: 'debruijn', icon: Icons.bolt),
|
||||
BruterProtocol(menuId: 39, name: 'DeBruijn EV1527', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.bolt),
|
||||
BruterProtocol(menuId: 40, name: 'Universal Sweep', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.radar),
|
||||
BruterProtocol(menuId: 35, name: 'DeBruijn Generic 433', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.bolt, te: 300, ratio: 3),
|
||||
BruterProtocol(menuId: 36, name: 'DeBruijn Generic 315', category: 'De Bruijn', frequencyMhz: 315.0, bits: 12, encoding: 'debruijn', icon: Icons.bolt, te: 300, ratio: 3),
|
||||
BruterProtocol(menuId: 37, name: 'DeBruijn Holtek', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.bolt, te: 430, ratio: 2),
|
||||
BruterProtocol(menuId: 38, name: 'DeBruijn Linear', category: 'De Bruijn', frequencyMhz: 300.0, bits: 10, encoding: 'debruijn', icon: Icons.bolt, te: 500, ratio: 3),
|
||||
BruterProtocol(menuId: 39, name: 'DeBruijn EV1527', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.bolt, te: 320, ratio: 3),
|
||||
BruterProtocol(menuId: 40, name: 'Universal Sweep', category: 'De Bruijn', frequencyMhz: 433.92, bits: 12, encoding: 'debruijn', icon: Icons.radar, te: 300, ratio: 3),
|
||||
];
|
||||
|
||||
/// Get unique category list preserving order
|
||||
@@ -169,7 +175,11 @@ class BruteScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
/// Map from standard protocol menuId to its De Bruijn equivalent menuId.
|
||||
/// Only protocols with binary encoding and ≤16 bits have De Bruijn support.
|
||||
/// NOTE: This map is kept for reference only. In De Bruijn mode, the app now
|
||||
/// sends a custom 0xFD command with the protocol's own Te, ratio, bits, and
|
||||
/// frequency — ensuring correct per-protocol timing and frequency.
|
||||
/// The hardcoded De Bruijn menus (35-39) remain available as standalone entries.
|
||||
// ignore: unused_element
|
||||
const Map<int, int> _standardToDeBruijnMap = {
|
||||
// CAME, NiceFlo, FAAC, BFT, Clemsa, GateTX, Phox, PhoenixV2, Prastel,
|
||||
// Doitrand, Nero, Magellen, Ansonic, EV1527 12b, Honeywell, StarLine,
|
||||
@@ -731,23 +741,17 @@ class _BruteScreenState extends State<BruteScreen> {
|
||||
final settingsProvider = Provider.of<SettingsProvider>(context, listen: false);
|
||||
final delayMs = settingsProvider.bruterDelayMs;
|
||||
|
||||
// Determine actual menu ID: if DeBruijn mode is on and protocol is compatible,
|
||||
// route to the corresponding DeBruijn menu instead.
|
||||
// Determine if we should use custom De Bruijn (per-protocol timing/freq)
|
||||
// instead of hardcoded De Bruijn menus which have fixed frequencies.
|
||||
bool useCustomDeBruijn = false;
|
||||
int actualMenuId = protocol.menuId;
|
||||
String modeSuffix = '';
|
||||
if (_useDeBruijnMode && !protocol.isDeBruijn && protocol.deBruijnCompatible) {
|
||||
final dbMenuId = _standardToDeBruijnMap[protocol.menuId];
|
||||
if (dbMenuId != null) {
|
||||
actualMenuId = dbMenuId;
|
||||
modeSuffix = ' (DeBruijn)';
|
||||
}
|
||||
useCustomDeBruijn = true;
|
||||
modeSuffix = ' (DeBruijn)';
|
||||
}
|
||||
|
||||
// Use the correct protocol for time estimation
|
||||
final displayProto = actualMenuId != protocol.menuId
|
||||
? bruterProtocols.firstWhere((p) => p.menuId == actualMenuId, orElse: () => protocol)
|
||||
: protocol;
|
||||
final estTime = displayProto.estimatedTimeWithDelay(delayMs);
|
||||
final estTime = protocol.estimatedTimeWithDelay(delayMs);
|
||||
|
||||
// Show confirmation dialog with protocol details
|
||||
final confirmed = await showDialog<bool>(
|
||||
@@ -811,7 +815,17 @@ class _BruteScreenState extends State<BruteScreen> {
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
try {
|
||||
await bleProvider.sendBruterCommand(actualMenuId);
|
||||
if (useCustomDeBruijn) {
|
||||
// Send custom De Bruijn command with per-protocol timing and frequency
|
||||
await bleProvider.sendCustomDeBruijnCommand(
|
||||
bits: protocol.bits,
|
||||
te: protocol.te,
|
||||
ratio: protocol.ratio,
|
||||
frequencyMhz: protocol.frequencyMhz,
|
||||
);
|
||||
} else {
|
||||
await bleProvider.sendBruterCommand(actualMenuId);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
final notificationProvider = Provider.of<NotificationProvider>(context, listen: false);
|
||||
|
||||
@@ -82,36 +82,14 @@ class _FileViewerScreenState extends State<FileViewerScreen>
|
||||
|
||||
// Log file path for debugging
|
||||
|
||||
// Determine basePath from pathType
|
||||
String basePath;
|
||||
switch (widget.pathType) {
|
||||
case 1:
|
||||
basePath = '/DATA/SIGNALS';
|
||||
break;
|
||||
case 2:
|
||||
basePath = '/DATA/PRESETS';
|
||||
break;
|
||||
case 3:
|
||||
basePath = '/DATA/TEMP';
|
||||
break;
|
||||
case 4:
|
||||
basePath = '/';
|
||||
break;
|
||||
default:
|
||||
basePath = '/DATA/RECORDS';
|
||||
}
|
||||
|
||||
// Use full path with directory (widget.filePath already contains the relative path)
|
||||
// Remove leading slash if present to get relative path
|
||||
// Use filePath as-is and let readFileContent handle path construction
|
||||
// based on pathType (0-3=relative, 4-5=absolute)
|
||||
String filePath = widget.filePath;
|
||||
if (filePath.startsWith('/')) {
|
||||
filePath = filePath.substring(1);
|
||||
}
|
||||
|
||||
print('Loading file: path="$filePath", pathType=${widget.pathType}');
|
||||
|
||||
// Read file from ESP (isLoadingFileContent flag is set internally)
|
||||
final content = await bleProvider.readFileContent(filePath, basePath: basePath);
|
||||
// Read file from ESP (pathType determines how path is interpreted)
|
||||
final content = await bleProvider.readFileContent(filePath, pathType: widget.pathType);
|
||||
|
||||
if (mounted) {
|
||||
// Check if response is an error from ESP
|
||||
|
||||
@@ -48,7 +48,7 @@ class _RecordScreenState extends State<RecordScreen> with TickerProviderStateMix
|
||||
|
||||
// Files from current recording session
|
||||
final List<String> _currentSessionFiles = [];
|
||||
|
||||
|
||||
// Flags for tracking changes
|
||||
final List<bool> _configsChanged = [];
|
||||
|
||||
@@ -95,7 +95,7 @@ class _RecordScreenState extends State<RecordScreen> with TickerProviderStateMix
|
||||
print('_onRecordedFilesChanged called');
|
||||
final runtimeFiles = _bleProvider?.recordedRuntimeFiles ?? [];
|
||||
print('Runtime files: $runtimeFiles');
|
||||
|
||||
|
||||
// Add new files to local recorded files list
|
||||
for (final file in runtimeFiles) {
|
||||
// Extract filename from object
|
||||
@@ -931,7 +931,7 @@ class _RecordScreenState extends State<RecordScreen> with TickerProviderStateMix
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
|
||||
// File list only for Recording
|
||||
if (selectedAction == ModuleAction.recording)
|
||||
_buildModuleFilesList(moduleIndex),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -28,6 +29,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
/// True after we sync HW button config from device once.
|
||||
bool _hwConfigSynced = false;
|
||||
|
||||
/// Debounce timer for nRF slider auto-send.
|
||||
Timer? _nrfDebounceTimer;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nrfDebounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Navigates to DebugScreen on single tap.
|
||||
void _onDebugTap(BuildContext context) {
|
||||
final settingsProvider =
|
||||
@@ -615,11 +625,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
bleProvider.settingsSynced ? AppLocalizations.of(context)!.syncedWithDevice : AppLocalizations.of(context)!.localOnly,
|
||||
style: TextStyle(
|
||||
color: bleProvider.settingsSynced
|
||||
? AppColors.success
|
||||
: AppColors.secondaryText,
|
||||
AppLocalizations.of(context)!.rfSettingsSubtitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.secondaryText,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
@@ -1027,73 +1035,79 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
Widget _buildScannerSettings(BuildContext context, BleProvider bleProvider) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
return Consumer<SettingsProvider>(
|
||||
builder: (context, settingsProvider, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.signal_cellular_alt,
|
||||
size: 18, color: AppColors.secondaryText),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.rssiThreshold(bleProvider.scannerRssi),
|
||||
style: const TextStyle(
|
||||
color: AppColors.primaryText,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
// ── RSSI threshold ──
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.signal_cellular_alt,
|
||||
size: 18, color: AppColors.secondaryText),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.rssiThreshold(bleProvider.scannerRssi),
|
||||
style: const TextStyle(
|
||||
color: AppColors.primaryText,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.minSignalStrengthDesc,
|
||||
style: const TextStyle(
|
||||
color: AppColors.secondaryText, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.minSignalStrengthDesc,
|
||||
style: const TextStyle(
|
||||
color: AppColors.secondaryText, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: bleProvider.scannerRssi.toDouble(),
|
||||
min: -120,
|
||||
max: -20,
|
||||
divisions: 100,
|
||||
label: '${bleProvider.scannerRssi} dBm',
|
||||
activeColor: AppColors.searching,
|
||||
onChanged: (value) {
|
||||
bleProvider.sendSettingsToDevice(scannerRssi: value.round());
|
||||
},
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [-90, -80, -70, -60, -50].map((rssi) {
|
||||
final isSelected = bleProvider.scannerRssi == rssi;
|
||||
return ChoiceChip(
|
||||
label: Text('$rssi'),
|
||||
selected: isSelected,
|
||||
onSelected: (_) {
|
||||
bleProvider.sendSettingsToDevice(scannerRssi: rssi);
|
||||
},
|
||||
selectedColor:
|
||||
AppColors.searching.withValues(alpha: 0.2),
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? AppColors.searching
|
||||
: AppColors.secondaryText,
|
||||
fontSize: 11,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: bleProvider.scannerRssi.toDouble(),
|
||||
min: -120,
|
||||
max: -20,
|
||||
divisions: 100,
|
||||
label: '${bleProvider.scannerRssi} dBm',
|
||||
activeColor: AppColors.searching,
|
||||
onChanged: (value) {
|
||||
bleProvider.sendSettingsToDevice(scannerRssi: value.round());
|
||||
},
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [-90, -80, -70, -60, -50].map((rssi) {
|
||||
final isSelected = bleProvider.scannerRssi == rssi;
|
||||
return ChoiceChip(
|
||||
label: Text('$rssi'),
|
||||
selected: isSelected,
|
||||
onSelected: (_) {
|
||||
bleProvider.sendSettingsToDevice(scannerRssi: rssi);
|
||||
},
|
||||
selectedColor:
|
||||
AppColors.searching.withValues(alpha: 0.2),
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? AppColors.searching
|
||||
: AppColors.secondaryText,
|
||||
fontSize: 11,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1178,7 +1192,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
return ChoiceChip(
|
||||
label: Text(_nrfPaLabel(lvl)),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => settingsProvider.setNrfPaLevel(lvl),
|
||||
onSelected: (_) {
|
||||
settingsProvider.setNrfPaLevel(lvl);
|
||||
_sendNrfSettings(context, bleProvider, settingsProvider);
|
||||
},
|
||||
selectedColor: const Color(0xFF00BCD4).withValues(alpha: 0.2),
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? const Color(0xFF00BCD4) : AppColors.secondaryText,
|
||||
@@ -1220,7 +1237,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
return ChoiceChip(
|
||||
label: Text(_nrfDataRateLabel(dr)),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => settingsProvider.setNrfDataRate(dr),
|
||||
onSelected: (_) {
|
||||
settingsProvider.setNrfDataRate(dr);
|
||||
_sendNrfSettings(context, bleProvider, settingsProvider);
|
||||
},
|
||||
selectedColor: const Color(0xFF00BCD4).withValues(alpha: 0.2),
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? const Color(0xFF00BCD4) : AppColors.secondaryText,
|
||||
@@ -1271,6 +1291,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
activeColor: const Color(0xFF00BCD4),
|
||||
onChanged: (value) {
|
||||
settingsProvider.setNrfChannel(value.round());
|
||||
_debouncedSendNrfSettings(context, bleProvider, settingsProvider);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1313,38 +1334,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
activeColor: const Color(0xFF00BCD4),
|
||||
onChanged: (value) {
|
||||
settingsProvider.setNrfAutoRetransmit(value.round());
|
||||
_debouncedSendNrfSettings(context, bleProvider, settingsProvider);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Send to device button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: bleProvider.isConnected
|
||||
? () => _sendNrfSettings(context, bleProvider, settingsProvider)
|
||||
: null,
|
||||
icon: const Icon(Icons.send),
|
||||
label: Text(AppLocalizations.of(context)!.sendToDevice),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00BCD4),
|
||||
foregroundColor: AppColors.primaryBackground,
|
||||
disabledBackgroundColor:
|
||||
const Color(0xFF00BCD4).withValues(alpha: 0.3),
|
||||
disabledForegroundColor: AppColors.disabledText,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!bleProvider.isConnected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.connectToDeviceToApply,
|
||||
style: const TextStyle(
|
||||
color: AppColors.secondaryText, fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1375,8 +1367,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Debounced auto-send for nRF slider changes (500ms).
|
||||
void _debouncedSendNrfSettings(BuildContext context, BleProvider bleProvider,
|
||||
SettingsProvider settingsProvider) {
|
||||
_nrfDebounceTimer?.cancel();
|
||||
_nrfDebounceTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
_sendNrfSettings(context, bleProvider, settingsProvider);
|
||||
});
|
||||
}
|
||||
|
||||
void _sendNrfSettings(BuildContext context, BleProvider bleProvider,
|
||||
SettingsProvider settingsProvider) async {
|
||||
if (!bleProvider.isConnected) return;
|
||||
try {
|
||||
// Send NRF settings as a settings sync command
|
||||
// Using MSG_SETTINGS_UPDATE (0xC1) with extended NRF payload
|
||||
@@ -1862,6 +1864,59 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Format SD Card ──
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.warning.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.sd_card_alert, size: 18, color: AppColors.warning),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Format SD Card',
|
||||
style: TextStyle(
|
||||
color: AppColors.warning,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Delete all files and folders from the SD card and re-create the default directory structure. This cannot be undone.',
|
||||
style: TextStyle(color: AppColors.secondaryText, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: bleProvider.isConnected
|
||||
? () => _showFormatSDDialog(context, bleProvider)
|
||||
: null,
|
||||
icon: const Icon(Icons.sd_card),
|
||||
label: const Text('Format SD Card'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.warning,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: AppColors.warning.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Factory Reset ──
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@@ -2049,6 +2104,158 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Show Format SD Card confirmation dialog, then a non-dismissible
|
||||
/// progress dialog that auto-closes when the firmware sends the result.
|
||||
void _showFormatSDDialog(BuildContext context, BleProvider bleProvider) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: AppColors.secondaryBackground,
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.sd_card_alert, color: AppColors.warning, size: 24),
|
||||
SizedBox(width: 10),
|
||||
Text('Format SD Card', style: TextStyle(color: AppColors.warning)),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'Are you sure you want to delete ALL files from the SD card?\n\n'
|
||||
'This will:\n'
|
||||
' - Delete all recordings, signals, and presets\n'
|
||||
' - Delete all uploaded .sub files\n'
|
||||
' - Re-create the default directory structure\n\n'
|
||||
'This action cannot be undone.',
|
||||
style: TextStyle(color: AppColors.primaryText, fontSize: 14),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('No', style: TextStyle(color: AppColors.secondaryText, fontSize: 16)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(ctx).pop();
|
||||
final sent = await bleProvider.formatSDCard();
|
||||
if (!context.mounted) return;
|
||||
if (!sent) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Failed to send format SD command.'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Show non-dismissible progress dialog; auto-closes on result
|
||||
_showSDFormatProgressDialog(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.warning,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Yes, Format', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Non-dismissible progress dialog that listens to BleProvider.isFormattingSD
|
||||
/// and closes automatically when the firmware result arrives.
|
||||
void _showSDFormatProgressDialog(BuildContext context) {
|
||||
bool closed = false;
|
||||
bool hasReceivedResponse = false;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => PopScope(
|
||||
canPop: false,
|
||||
child: Consumer<BleProvider>(
|
||||
builder: (context, ble, _) {
|
||||
// Mark that we've started receiving progress updates
|
||||
if (ble.isFormattingSD && ble.sdFormatProgress.isNotEmpty) {
|
||||
hasReceivedResponse = true;
|
||||
}
|
||||
|
||||
// Only close if we've received at least one progress update and formatting is done
|
||||
// This prevents premature closure if firmware responds instantly with error
|
||||
if (!ble.isFormattingSD && !closed && hasReceivedResponse) {
|
||||
closed = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (ctx.mounted) Navigator.of(ctx).pop();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(ble.sdFormatSuccess
|
||||
? 'SD card formatted successfully.'
|
||||
: 'SD card format failed.'),
|
||||
backgroundColor: ble.sdFormatSuccess
|
||||
? AppColors.success
|
||||
: AppColors.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show warning if formatting takes too long without progress
|
||||
if (ble.isFormattingSD && !hasReceivedResponse) {
|
||||
// Start a timeout to close dialog if no response after 10 seconds
|
||||
Future.delayed(const Duration(seconds: 10), () {
|
||||
if (ctx.mounted && !closed && !hasReceivedResponse) {
|
||||
closed = true;
|
||||
Navigator.of(ctx).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Format timeout: No response from device.'),
|
||||
backgroundColor: AppColors.error,
|
||||
duration: Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: AppColors.secondaryBackground,
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.sd_card, color: AppColors.warning, size: 24),
|
||||
SizedBox(width: 10),
|
||||
Text('Formatting...', style: TextStyle(color: AppColors.warning)),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
ble.sdFormatProgress.isNotEmpty
|
||||
? ble.sdFormatProgress
|
||||
: 'Formatting SD card, please wait.',
|
||||
style: const TextStyle(color: AppColors.primaryText),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Do not disconnect the device.',
|
||||
style: TextStyle(color: AppColors.secondaryText, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build HW Buttons configuration section.
|
||||
Widget _buildHwButtonsSection(BuildContext context, BleProvider bleProvider) {
|
||||
return Card(
|
||||
@@ -2124,9 +2331,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
action: settingsProvider.button1Action,
|
||||
color: AppColors.primaryAccent,
|
||||
replayPath: settingsProvider.button1ReplayPath,
|
||||
onPickReplayFile: () => _pickReplaySubFile(context, settingsProvider, 1),
|
||||
onPickReplayFile: () async {
|
||||
await _pickReplaySubFile(context, settingsProvider, 1);
|
||||
_sendButtonConfig(context, bleProvider, settingsProvider);
|
||||
},
|
||||
onChanged: (action) {
|
||||
settingsProvider.setButton1Action(action);
|
||||
_sendButtonConfig(context, bleProvider, settingsProvider);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2138,42 +2349,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
action: settingsProvider.button2Action,
|
||||
color: AppColors.warning,
|
||||
replayPath: settingsProvider.button2ReplayPath,
|
||||
onPickReplayFile: () => _pickReplaySubFile(context, settingsProvider, 2),
|
||||
onPickReplayFile: () async {
|
||||
await _pickReplaySubFile(context, settingsProvider, 2);
|
||||
_sendButtonConfig(context, bleProvider, settingsProvider);
|
||||
},
|
||||
onChanged: (action) {
|
||||
settingsProvider.setButton2Action(action);
|
||||
_sendButtonConfig(context, bleProvider, settingsProvider);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Send to device button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: bleProvider.isConnected
|
||||
? () => _sendButtonConfig(
|
||||
context, bleProvider, settingsProvider)
|
||||
: null,
|
||||
icon: const Icon(Icons.send),
|
||||
label: Text(AppLocalizations.of(context)!.sendToDevice),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.info,
|
||||
foregroundColor: AppColors.primaryBackground,
|
||||
disabledBackgroundColor:
|
||||
AppColors.info.withValues(alpha: 0.3),
|
||||
disabledForegroundColor: AppColors.disabledText,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!bleProvider.isConnected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.connectToDeviceToApply,
|
||||
style: const TextStyle(
|
||||
color: AppColors.secondaryText, fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -2261,7 +2445,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
child: Text(
|
||||
replayPath == null || replayPath.isEmpty
|
||||
? 'No .sub file selected'
|
||||
: replayPath,
|
||||
: replayPath.split('/').last,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: AppColors.secondaryText, fontSize: 11),
|
||||
@@ -3150,34 +3334,126 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
bool _hasError = false;
|
||||
String _errorMessage = '';
|
||||
|
||||
// Pause / Resume state
|
||||
bool _isPaused = false;
|
||||
bool _pauseRequested = false;
|
||||
bool _checkingResume = true;
|
||||
bool _hasResumableSession = false;
|
||||
Set<String> _completedPaths = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startCloning();
|
||||
_checkForResumableSession();
|
||||
}
|
||||
|
||||
Future<void> _startCloning() async {
|
||||
/// On open, check if a previous session can be resumed.
|
||||
Future<void> _checkForResumableSession() async {
|
||||
final hasResume = await FlipperSubDbService.hasResumableSession();
|
||||
if (!mounted) return;
|
||||
if (hasResume) {
|
||||
final completed = await FlipperSubDbService.loadCompletedFiles();
|
||||
setState(() {
|
||||
_checkingResume = false;
|
||||
_hasResumableSession = true;
|
||||
_completedPaths = completed;
|
||||
_statusText =
|
||||
'Previous session found (${completed.length} files already uploaded). Resume or start fresh?';
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_checkingResume = false;
|
||||
_hasResumableSession = false;
|
||||
});
|
||||
_startCloning(resume: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pause the current upload. Finishes the file in progress, then stops.
|
||||
void _pauseCloning() {
|
||||
setState(() {
|
||||
_pauseRequested = true;
|
||||
_statusText = 'Pausing after current file...';
|
||||
});
|
||||
}
|
||||
|
||||
/// Resume a paused or previously-saved session.
|
||||
void _resumeCloning() {
|
||||
setState(() {
|
||||
_isPaused = false;
|
||||
_pauseRequested = false;
|
||||
_hasResumableSession = false;
|
||||
});
|
||||
_startCloning(resume: true);
|
||||
}
|
||||
|
||||
Future<void> _startCloning({required bool resume}) async {
|
||||
// Keep screen awake during the entire cloning process
|
||||
WakelockPlus.enable();
|
||||
try {
|
||||
// Phase 1 & 2: Download and extract
|
||||
setState(() {
|
||||
_phase = 'download';
|
||||
_statusText = 'Downloading SubGHz database from GitHub...';
|
||||
_progress = 0.0;
|
||||
});
|
||||
List<SubFileEntry> subFiles;
|
||||
|
||||
final subFiles = await FlipperSubDbService.downloadAndExtract(
|
||||
onProgress: (phase, detail, fraction) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_phase = phase;
|
||||
_statusText = detail;
|
||||
_progress = fraction;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
if (resume) {
|
||||
// ── Resume path: re-extract from cached ZIP ──
|
||||
setState(() {
|
||||
_phase = 'extract';
|
||||
_statusText = 'Loading cached database...';
|
||||
_progress = 0.0;
|
||||
});
|
||||
|
||||
final cachedZip = await FlipperSubDbService.loadCachedZip();
|
||||
if (cachedZip == null) {
|
||||
setState(() {
|
||||
_isDone = true;
|
||||
_hasError = true;
|
||||
_errorMessage = 'Cached ZIP not found. Please start a fresh clone.';
|
||||
});
|
||||
WakelockPlus.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load previously completed files
|
||||
_completedPaths = await FlipperSubDbService.loadCompletedFiles();
|
||||
|
||||
subFiles = FlipperSubDbService.extractFromBytes(
|
||||
cachedZip,
|
||||
onProgress: (phase, detail, fraction) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_phase = phase;
|
||||
_statusText = detail;
|
||||
_progress = fraction;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// ── Fresh start: download and extract ──
|
||||
_completedPaths = {};
|
||||
await FlipperSubDbService.clearCache();
|
||||
|
||||
setState(() {
|
||||
_phase = 'download';
|
||||
_statusText = 'Downloading SubGHz database from GitHub...';
|
||||
_progress = 0.0;
|
||||
});
|
||||
|
||||
subFiles = await FlipperSubDbService.downloadAndExtract(
|
||||
onProgress: (phase, detail, fraction) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_phase = phase;
|
||||
_statusText = detail;
|
||||
_progress = fraction;
|
||||
});
|
||||
}
|
||||
},
|
||||
onZipDownloaded: (zipBytes) async {
|
||||
// Cache the raw ZIP for potential resume
|
||||
await FlipperSubDbService.cacheZipBytes(zipBytes);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (subFiles.isEmpty) {
|
||||
setState(() {
|
||||
@@ -3185,16 +3461,18 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
_hasError = true;
|
||||
_errorMessage = 'No .sub files found in the repository';
|
||||
});
|
||||
WakelockPlus.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
_totalFiles = subFiles.length;
|
||||
_uploadedFiles = _completedPaths.length;
|
||||
|
||||
// Phase 3: Create base directory on SDCard
|
||||
setState(() {
|
||||
_phase = 'upload';
|
||||
_statusText = 'Creating "SUB Files" folder on SDCard...';
|
||||
_progress = 0.0;
|
||||
_progress = _completedPaths.length / _totalFiles;
|
||||
});
|
||||
|
||||
await widget.bleProvider.createDirectory(
|
||||
@@ -3208,7 +3486,8 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
if (parts.length > 1) {
|
||||
// Build cumulative subdir paths
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
final subdir = '${FlipperSubDbService.sdTargetFolder}/${parts.sublist(0, i).join('/')}';
|
||||
final subdir =
|
||||
'${FlipperSubDbService.sdTargetFolder}/${parts.sublist(0, i).join('/')}';
|
||||
subdirs.add(subdir);
|
||||
}
|
||||
}
|
||||
@@ -3217,6 +3496,7 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
// Create subdirectories (sorted so parents come first)
|
||||
final sortedDirs = subdirs.toList()..sort();
|
||||
for (final dir in sortedDirs) {
|
||||
if (_pauseRequested) break;
|
||||
setState(() {
|
||||
_statusText = 'Creating folder: $dir';
|
||||
});
|
||||
@@ -3230,15 +3510,36 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
|
||||
// Phase 4: Upload files one at a time
|
||||
for (int i = 0; i < subFiles.length; i++) {
|
||||
// ── Check for pause after each file ──
|
||||
if (_pauseRequested) {
|
||||
await FlipperSubDbService.saveProgress(_completedPaths);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isPaused = true;
|
||||
_pauseRequested = false;
|
||||
_statusText =
|
||||
'Paused – $_uploadedFiles / $_totalFiles files uploaded. You can close and resume later.';
|
||||
});
|
||||
}
|
||||
WakelockPlus.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
final file = subFiles[i];
|
||||
|
||||
// Skip already-uploaded files (from a previous session)
|
||||
if (_completedPaths.contains(file.relativePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final targetPath =
|
||||
'${FlipperSubDbService.sdTargetFolder}/${file.relativePath}';
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_uploadedFiles = i;
|
||||
_statusText = 'Uploading (${i + 1}/$_totalFiles): ${file.relativePath}';
|
||||
_progress = i / _totalFiles;
|
||||
_statusText =
|
||||
'Uploading (${_uploadedFiles + 1}/$_totalFiles): ${file.relativePath}';
|
||||
_progress = _uploadedFiles / _totalFiles;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3248,6 +3549,14 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
targetPath,
|
||||
pathType: 5,
|
||||
);
|
||||
_completedPaths.add(file.relativePath);
|
||||
_uploadedFiles = _completedPaths.length;
|
||||
|
||||
// Persist progress every 10 files for safety
|
||||
if (_uploadedFiles % 10 == 0) {
|
||||
await FlipperSubDbService.saveProgress(_completedPaths);
|
||||
}
|
||||
|
||||
// Pace uploads to avoid BLE congestion
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
} catch (e) {
|
||||
@@ -3257,6 +3566,9 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
// All files processed – clean up cache
|
||||
await FlipperSubDbService.clearCache();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDone = true;
|
||||
@@ -3270,6 +3582,10 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
// Release wakelock after successful completion
|
||||
WakelockPlus.disable();
|
||||
} catch (e) {
|
||||
// On error, save progress so user can resume later
|
||||
if (_completedPaths.isNotEmpty) {
|
||||
await FlipperSubDbService.saveProgress(_completedPaths);
|
||||
}
|
||||
// Release wakelock on error
|
||||
WakelockPlus.disable();
|
||||
if (mounted) {
|
||||
@@ -3285,23 +3601,87 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// While checking for resumable session, show a spinner
|
||||
if (_checkingResume) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Clone SubGHz DB', style: TextStyle(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
content: const Text('Checking for previous session...'),
|
||||
);
|
||||
}
|
||||
|
||||
// If a resumable session was found, show Resume / Start Fresh options
|
||||
if (_hasResumableSession && !_isPaused && _phase == 'init') {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.replay, color: Colors.orange),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(
|
||||
child: Text('Resume Clone?', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
'${_completedPaths.length} files were uploaded in a previous session.\n'
|
||||
'Resume where you left off, or start fresh?',
|
||||
style: TextStyle(color: AppColors.primaryText, fontSize: 13),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_hasResumableSession = false;
|
||||
_completedPaths = {};
|
||||
});
|
||||
_startCloning(resume: false);
|
||||
},
|
||||
child: const Text('Start Fresh'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _resumeCloning,
|
||||
child: const Text('Resume'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_isDone
|
||||
? (_hasError ? Icons.error_outline : Icons.check_circle)
|
||||
: Icons.cloud_download,
|
||||
: _isPaused
|
||||
? Icons.pause_circle
|
||||
: Icons.cloud_download,
|
||||
color: _isDone
|
||||
? (_hasError ? AppColors.error : AppColors.success)
|
||||
: Colors.orange,
|
||||
: _isPaused
|
||||
? Colors.orange
|
||||
: Colors.orange,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_isDone
|
||||
? (_hasError ? 'Clone Failed' : 'Clone Complete')
|
||||
: 'Cloning SubGHz Database',
|
||||
: _isPaused
|
||||
? 'Clone Paused'
|
||||
: 'Cloning SubGHz Database',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
@@ -3312,7 +3692,7 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Phase indicator
|
||||
if (!_isDone) ...[
|
||||
if (!_isDone && !_isPaused) ...[
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -3364,7 +3744,9 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
? AppColors.error
|
||||
: _isDone
|
||||
? AppColors.success
|
||||
: Colors.orange,
|
||||
: _isPaused
|
||||
? Colors.orange.shade300
|
||||
: Colors.orange,
|
||||
),
|
||||
minHeight: 8,
|
||||
),
|
||||
@@ -3372,7 +3754,7 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// File counter (during upload phase)
|
||||
if (_phase == 'upload' || _isDone)
|
||||
if (_phase == 'upload' || _isDone || _isPaused)
|
||||
Text(
|
||||
'$_uploadedFiles / $_totalFiles files'
|
||||
'${_failedFiles > 0 ? ' ($_failedFiles failed)' : ''}',
|
||||
@@ -3402,10 +3784,41 @@ class _SubGhzCloneDialogState extends State<_SubGhzCloneDialog> {
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (_isDone)
|
||||
ElevatedButton(
|
||||
// Close button when done, paused, or errored
|
||||
if (_isDone || _isPaused)
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Close'),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
|
||||
// Resume button when paused
|
||||
if (_isPaused)
|
||||
ElevatedButton.icon(
|
||||
onPressed: _resumeCloning,
|
||||
icon: const Icon(Icons.play_arrow, size: 18),
|
||||
label: const Text('Resume'),
|
||||
),
|
||||
|
||||
// Pause button during upload (only while actively uploading)
|
||||
if (!_isDone && !_isPaused && _phase == 'upload' && !_pauseRequested)
|
||||
ElevatedButton.icon(
|
||||
onPressed: _pauseCloning,
|
||||
icon: const Icon(Icons.pause, size: 18),
|
||||
label: const Text('Pause'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
),
|
||||
|
||||
// Show "Pausing..." indicator when pause is requested
|
||||
if (_pauseRequested && !_isPaused)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import '../providers/notification_provider.dart';
|
||||
import '../providers/firmware_protocol.dart';
|
||||
import '../models/detected_signal.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
class SignalScannerScreen extends StatefulWidget {
|
||||
const SignalScannerScreen({super.key});
|
||||
|
||||
@@ -19,8 +18,8 @@ class SignalScannerScreen extends StatefulWidget {
|
||||
class _SignalScannerScreenState extends State<SignalScannerScreen>
|
||||
with TickerProviderStateMixin {
|
||||
|
||||
// Scanning modes
|
||||
bool _isListMode = true;
|
||||
// View modes: 0 = Signal List, 1 = Spectrogram (default)
|
||||
int _viewMode = 1;
|
||||
bool _isScanning = false;
|
||||
int _selectedModule = 0; // 0 or 1 (displayed as 1 or 2)
|
||||
|
||||
@@ -53,7 +52,7 @@ class _SignalScannerScreenState extends State<SignalScannerScreen>
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
)..addListener(() {
|
||||
if (!_isListMode) setState(() {}); // Rebuild spectrogram on tick
|
||||
if (_viewMode != 0) setState(() {}); // Rebuild spectrogram on tick
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,7 +74,7 @@ class _SignalScannerScreenState extends State<SignalScannerScreen>
|
||||
final bleProvider = Provider.of<BleProvider>(context, listen: false);
|
||||
|
||||
setState(() => _isScanning = true);
|
||||
if (!_isListMode) _spectrumAnimationController.repeat();
|
||||
if (_viewMode != 0) _spectrumAnimationController.repeat();
|
||||
|
||||
// Start decay timer for dynamic spectrogram
|
||||
_startDecayTimer();
|
||||
@@ -261,7 +260,7 @@ class _SignalScannerScreenState extends State<SignalScannerScreen>
|
||||
_buildControlPanel(),
|
||||
_buildModeSwitch(),
|
||||
Expanded(
|
||||
child: _isListMode
|
||||
child: _viewMode == 0
|
||||
? _buildListView(bleProvider)
|
||||
: _buildSpectrumView(bleProvider),
|
||||
),
|
||||
@@ -313,7 +312,7 @@ class _SignalScannerScreenState extends State<SignalScannerScreen>
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// RSSI threshold
|
||||
if (_isListMode)
|
||||
if (_viewMode == 0)
|
||||
Row(
|
||||
children: [
|
||||
const Text('RSSI:', style: TextStyle(
|
||||
@@ -374,29 +373,28 @@ class _SignalScannerScreenState extends State<SignalScannerScreen>
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_modeSwitchTab(AppLocalizations.of(context)!.signalList, isSelected: _isListMode, onTap: () {
|
||||
setState(() => _isListMode = true);
|
||||
}),
|
||||
_modeSwitchTab(AppLocalizations.of(context)!.spectrogramView, isSelected: !_isListMode, onTap: () {
|
||||
setState(() => _isListMode = false);
|
||||
}),
|
||||
_modeSwitchTab(AppLocalizations.of(context)!.signalList,
|
||||
isSelected: _viewMode == 0, onTap: () => _setViewMode(0)),
|
||||
_modeSwitchTab(AppLocalizations.of(context)!.spectrogramView,
|
||||
isSelected: _viewMode == 1, onTap: () => _setViewMode(1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setViewMode(int mode) {
|
||||
setState(() => _viewMode = mode);
|
||||
if (_viewMode != 0 && _isScanning) {
|
||||
_spectrumAnimationController.repeat();
|
||||
} else {
|
||||
_spectrumAnimationController.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _modeSwitchTab(String label, {required bool isSelected, required VoidCallback onTap}) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTap();
|
||||
// Start/stop animation controller when switching to/from spectrogram
|
||||
if (!_isListMode && _isScanning) {
|
||||
_spectrumAnimationController.repeat();
|
||||
} else {
|
||||
_spectrumAnimationController.stop();
|
||||
}
|
||||
},
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
@@ -409,7 +407,7 @@ class _SignalScannerScreenState extends State<SignalScannerScreen>
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.black : AppColors.secondaryText,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -430,16 +430,21 @@ class BinaryFileActionResult {
|
||||
case 5: return 'move';
|
||||
case 6: return 'tree';
|
||||
case 7: return 'load';
|
||||
case 8: return 'format-sd';
|
||||
default: return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
// errorCode 0xFF with success=true means "in progress" (format SD step)
|
||||
bool isProgress = (success && errorCode == 0xFF);
|
||||
return {
|
||||
'action': getActionString(),
|
||||
'success': success,
|
||||
'path': path,
|
||||
'error': success ? null : _getErrorMessage(errorCode),
|
||||
'isProgress': isProgress,
|
||||
'progressMessage': isProgress ? path : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class FlipperSubDbService {
|
||||
/// - phase "extract": extracting .sub files from ZIP
|
||||
static Future<List<SubFileEntry>> downloadAndExtract({
|
||||
void Function(String phase, String detail, double fraction)? onProgress,
|
||||
Future<void> Function(List<int> zipBytes)? onZipDownloaded,
|
||||
}) async {
|
||||
// --- Phase 1: Download ZIP ---
|
||||
onProgress?.call('download', 'Connecting to GitHub...', 0.0);
|
||||
@@ -65,6 +66,9 @@ class FlipperSubDbService {
|
||||
|
||||
onProgress?.call('download', 'Download complete', 1.0);
|
||||
|
||||
// Allow caller to cache the raw ZIP for later resume
|
||||
await onZipDownloaded?.call(zipBytes);
|
||||
|
||||
// --- Phase 2: Extract .sub files ---
|
||||
onProgress?.call('extract', 'Decompressing ZIP...', 0.0);
|
||||
|
||||
@@ -72,8 +76,12 @@ class FlipperSubDbService {
|
||||
final subFiles = <SubFileEntry>[];
|
||||
|
||||
// The ZIP contains a root folder like "FlipperZero-Subghz-DB-main/"
|
||||
// We strip that prefix to get clean relative paths.
|
||||
// followed by a "subghz/" subfolder. We strip both prefixes so that
|
||||
// relative paths start directly at the category level (e.g.,
|
||||
// "Adjustable_Beds/RIZE .../file.sub") and map onto "SUB Files/..." on SD.
|
||||
String? rootPrefix;
|
||||
// Second-level prefix to strip (e.g. "subghz/")
|
||||
const String subghzFolder = 'subghz/';
|
||||
|
||||
int processed = 0;
|
||||
final total = archive.files.length;
|
||||
@@ -92,6 +100,11 @@ class FlipperSubDbService {
|
||||
if (rootPrefix != null && relativePath.startsWith(rootPrefix)) {
|
||||
relativePath = relativePath.substring(rootPrefix.length);
|
||||
}
|
||||
// Strip the "subghz/" second-level folder so paths go directly
|
||||
// inside "SUB Files/" on the SD card.
|
||||
if (relativePath.startsWith(subghzFolder)) {
|
||||
relativePath = relativePath.substring(subghzFolder.length);
|
||||
}
|
||||
// Skip empty paths
|
||||
if (relativePath.isNotEmpty) {
|
||||
subFiles.add(SubFileEntry(
|
||||
@@ -129,4 +142,119 @@ class FlipperSubDbService {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Progress persistence for Pause / Resume
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const String _progressFileName = 'clone_progress.json';
|
||||
static const String _cachedZipFileName = 'clone_cached.zip';
|
||||
|
||||
/// Return the app-data directory used for clone cache files.
|
||||
static Future<Directory> _cacheDir() async {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${appDir.path}/clone_cache');
|
||||
if (!await dir.exists()) await dir.create(recursive: true);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// Check whether a resumable clone session exists.
|
||||
static Future<bool> hasResumableSession() async {
|
||||
final dir = await _cacheDir();
|
||||
final progressFile = File('${dir.path}/$_progressFileName');
|
||||
final zipFile = File('${dir.path}/$_cachedZipFileName');
|
||||
return await progressFile.exists() && await zipFile.exists();
|
||||
}
|
||||
|
||||
/// Load the set of already-uploaded relative paths from the progress file.
|
||||
static Future<Set<String>> loadCompletedFiles() async {
|
||||
final dir = await _cacheDir();
|
||||
final file = File('${dir.path}/$_progressFileName');
|
||||
if (!await file.exists()) return {};
|
||||
try {
|
||||
final json = jsonDecode(await file.readAsString());
|
||||
return Set<String>.from(json['completed'] as List);
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the set of completed file paths (call after each successful upload).
|
||||
static Future<void> saveProgress(Set<String> completedPaths) async {
|
||||
final dir = await _cacheDir();
|
||||
final file = File('${dir.path}/$_progressFileName');
|
||||
await file.writeAsString(jsonEncode({'completed': completedPaths.toList()}));
|
||||
}
|
||||
|
||||
/// Cache the raw ZIP bytes so we don't have to re-download on resume.
|
||||
static Future<void> cacheZipBytes(List<int> zipBytes) async {
|
||||
final dir = await _cacheDir();
|
||||
final file = File('${dir.path}/$_cachedZipFileName');
|
||||
await file.writeAsBytes(zipBytes);
|
||||
}
|
||||
|
||||
/// Load cached ZIP bytes for resume.
|
||||
static Future<List<int>?> loadCachedZip() async {
|
||||
final dir = await _cacheDir();
|
||||
final file = File('${dir.path}/$_cachedZipFileName');
|
||||
if (!await file.exists()) return null;
|
||||
return await file.readAsBytes();
|
||||
}
|
||||
|
||||
/// Delete all clone cache files (call on completion or manual reset).
|
||||
static Future<void> clearCache() async {
|
||||
final dir = await _cacheDir();
|
||||
if (await dir.exists()) await dir.delete(recursive: true);
|
||||
}
|
||||
|
||||
/// Extract .sub files from already-downloaded ZIP bytes.
|
||||
/// Same logic as [downloadAndExtract] phase 2, but without downloading.
|
||||
static List<SubFileEntry> extractFromBytes(
|
||||
List<int> zipBytes, {
|
||||
void Function(String phase, String detail, double fraction)? onProgress,
|
||||
}) {
|
||||
onProgress?.call('extract', 'Decompressing cached ZIP...', 0.0);
|
||||
|
||||
final archive = ZipDecoder().decodeBytes(zipBytes);
|
||||
final subFiles = <SubFileEntry>[];
|
||||
String? rootPrefix;
|
||||
const String subghzFolder = 'subghz/';
|
||||
|
||||
int processed = 0;
|
||||
final total = archive.files.length;
|
||||
|
||||
for (final file in archive.files) {
|
||||
processed++;
|
||||
if (file.isFile) {
|
||||
final name = file.name;
|
||||
rootPrefix ??= _extractRootPrefix(name);
|
||||
|
||||
if (name.toLowerCase().endsWith('.sub')) {
|
||||
String relativePath = name;
|
||||
if (rootPrefix != null && relativePath.startsWith(rootPrefix)) {
|
||||
relativePath = relativePath.substring(rootPrefix.length);
|
||||
}
|
||||
if (relativePath.startsWith(subghzFolder)) {
|
||||
relativePath = relativePath.substring(subghzFolder.length);
|
||||
}
|
||||
if (relativePath.isNotEmpty) {
|
||||
subFiles.add(SubFileEntry(
|
||||
relativePath: relativePath,
|
||||
content: Uint8List.fromList(file.content as List<int>),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (total > 0) {
|
||||
onProgress?.call(
|
||||
'extract',
|
||||
'Extracting files... (${subFiles.length} .sub files found)',
|
||||
processed / total,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.call('extract', '${subFiles.length} .sub files extracted', 1.0);
|
||||
return subFiles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ class UpdateService {
|
||||
String currentVersion) async {
|
||||
try {
|
||||
final releases = await _fetchReleases();
|
||||
// Fetch structured changelog in parallel
|
||||
final changelogData = await fetchChangelog();
|
||||
// Fetch structured changelog from release assets
|
||||
final changelogData = await fetchChangelog(releases);
|
||||
|
||||
String? bestVersion;
|
||||
Map<String, dynamic>? bestRelease;
|
||||
@@ -154,8 +154,8 @@ class UpdateService {
|
||||
static Future<AppUpdate?> checkAppUpdate(String currentVersion) async {
|
||||
try {
|
||||
final releases = await _fetchReleases();
|
||||
// Fetch structured changelog in parallel
|
||||
final changelogData = await fetchChangelog();
|
||||
// Fetch structured changelog from release assets
|
||||
final changelogData = await fetchChangelog(releases);
|
||||
|
||||
String? bestVersion;
|
||||
Map<String, dynamic>? bestRelease;
|
||||
@@ -329,25 +329,52 @@ class UpdateService {
|
||||
return await getTemporaryDirectory();
|
||||
}
|
||||
|
||||
/// URL to the structured changelog.json in the repository.
|
||||
static const String _changelogUrl =
|
||||
'https://raw.githubusercontent.com/$githubOwner/$githubRepo/main/releases/changelog.json';
|
||||
|
||||
/// Fetch the structured changelog from changelog.json.
|
||||
/// Fetch the structured changelog from changelog.json in release assets.
|
||||
/// Searches through releases to find changelog.json as an asset.
|
||||
/// Returns a map with "firmware" and "app" lists parsed from the JSON.
|
||||
/// Returns null on failure (non-fatal — falls back to release body).
|
||||
static Future<Map<String, dynamic>?> fetchChangelog() async {
|
||||
static Future<Map<String, dynamic>?> fetchChangelog(List<dynamic> releases) async {
|
||||
// Try to find changelog.json in release assets (newest first)
|
||||
for (final release in releases) {
|
||||
if (release['draft'] == true) continue;
|
||||
final assets = release['assets'] as List? ?? [];
|
||||
|
||||
// Look for changelog.json asset
|
||||
for (final asset in assets) {
|
||||
final name = asset['name'] as String? ?? '';
|
||||
if (name.toLowerCase() == 'changelog.json') {
|
||||
final downloadUrl = asset['browser_download_url'] as String?;
|
||||
if (downloadUrl == null) continue;
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse(downloadUrl),
|
||||
).timeout(_timeout);
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue searching in other releases
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try legacy URL from main branch (for backward compatibility)
|
||||
try {
|
||||
const legacyUrl = 'https://raw.githubusercontent.com/$githubOwner/$githubRepo/main/releases/changelog.json';
|
||||
final response = await http.get(
|
||||
Uri.parse(_changelogUrl),
|
||||
Uri.parse(legacyUrl),
|
||||
).timeout(_timeout);
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
// Ignore fallback errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extract structured changes list for a specific version from changelog.json.
|
||||
|
||||
@@ -169,18 +169,26 @@ class QuickConnectWidget extends StatelessWidget {
|
||||
} else {
|
||||
// No saved devices - show scan button or scan results
|
||||
List<dynamic> supportedDevices = bleProvider.supportedScanResults;
|
||||
if (supportedDevices.isNotEmpty) {
|
||||
// Show found supported devices
|
||||
|
||||
// Fallback: if no supported devices found but scan returned results,
|
||||
// show ALL nearby BLE devices so user can manually select (e.g. renamed device)
|
||||
final bool isFallback = supportedDevices.isEmpty && bleProvider.scanResults.isNotEmpty;
|
||||
final List<dynamic> devicesToShow = isFallback ? bleProvider.scanResults : supportedDevices;
|
||||
|
||||
if (devicesToShow.isNotEmpty) {
|
||||
// Show found devices
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.foundSupportedDevicesCount(supportedDevices.length),
|
||||
isFallback
|
||||
? AppLocalizations.of(context)!.noSupportedDevicesShowAll
|
||||
: AppLocalizations.of(context)!.foundSupportedDevicesCount(devicesToShow.length),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: AppColors.secondaryText,
|
||||
color: isFallback ? AppColors.warning : AppColors.secondaryText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...supportedDevices.map((result) => Container(
|
||||
...devicesToShow.map((result) => Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -190,8 +198,8 @@ class QuickConnectWidget extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.bluetooth,
|
||||
Icon(
|
||||
isFallback ? Icons.bluetooth_searching : Icons.bluetooth,
|
||||
color: AppColors.info,
|
||||
size: 20,
|
||||
),
|
||||
|
||||
@@ -96,7 +96,7 @@ class _StatusBarWidgetState extends State<StatusBarWidget> {
|
||||
right: 0,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.85,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: evilcrow_rf2_controller
|
||||
description: EvilCrow RF — Mobile app for controlling RF devices via BLE
|
||||
publish_to: 'none'
|
||||
version: 1.0.2+15
|
||||
version: 1.1.0+25
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
+1
-9
@@ -1,12 +1,4 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
|
||||
[env:esp32dev]
|
||||
platform = espressif32
|
||||
|
||||
@@ -633,14 +633,15 @@ bool BleAdapter::handleUploadChunk(uint8_t chunkId, uint8_t chunkNum, uint8_t to
|
||||
}
|
||||
|
||||
if (pathLength > 0) {
|
||||
if (pathType != 4) strcat(fullPath, "/");
|
||||
// pathType 4 (LittleFS root) and 5 (SD root) already end with '/'
|
||||
if (pathType != 4 && pathType != 5) strcat(fullPath, "/");
|
||||
if (path[0] == '/') {
|
||||
strncat(fullPath, path + 1, pathLength - 1);
|
||||
} else {
|
||||
strncat(fullPath, path, pathLength);
|
||||
}
|
||||
} else {
|
||||
if (pathType != 4) strcat(fullPath, "/");
|
||||
if (pathType != 4 && pathType != 5) strcat(fullPath, "/");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting file upload: %s (pathType=%d)", fullPath, pathType);
|
||||
|
||||
+24
-5
@@ -174,6 +174,7 @@ void cc1101WorkerSignalDetectedHandler(const CC1101DetectedSignal& signal)
|
||||
struct DeviceConfig
|
||||
{
|
||||
bool powerBlink;
|
||||
bool sdCardMounted; // True if SD card is available
|
||||
} deviceConfig;
|
||||
|
||||
// REMOVED - old state machine task (all code deleted, now using CC1101Worker)
|
||||
@@ -621,13 +622,31 @@ void setup()
|
||||
|
||||
ESP_LOGD(TAG, "Starting setup...");
|
||||
|
||||
// --- SD Card initialization (non-blocking) ---
|
||||
sdspi.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_SS);
|
||||
if (!SD.begin(SD_SS, sdspi)) {
|
||||
ESP_LOGE(TAG, "Card Mount Failed");
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "SD card not mounted — running in LittleFS-only mode");
|
||||
deviceConfig.sdCardMounted = false;
|
||||
// Continue setup without SD — features requiring SD will be limited
|
||||
} else {
|
||||
deviceConfig.sdCardMounted = true;
|
||||
ESP_LOGI(TAG, "SD card initialized.");
|
||||
|
||||
ESP_LOGD(TAG, "SD card initialized.");
|
||||
// Ensure default directory structure exists on SD
|
||||
static const char* defaultDirs[] = {
|
||||
"/DATA",
|
||||
"/DATA/RECORDS",
|
||||
"/DATA/SIGNALS",
|
||||
"/DATA/PRESETS",
|
||||
"/DATA/TEMP"
|
||||
};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (!SD.exists(defaultDirs[i])) {
|
||||
SD.mkdir(defaultDirs[i]);
|
||||
ESP_LOGI(TAG, "Created missing directory: %s", defaultDirs[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ControllerAdapter::initializeQueue();
|
||||
|
||||
@@ -716,7 +735,7 @@ void setup()
|
||||
#endif
|
||||
|
||||
// Notification sender on Core 0 (near BLE stack for lower latency)
|
||||
xTaskCreatePinnedToCore(ClientsManager::processMessageQueue, "SendNotifications", 2560, NULL, 1, NULL, 0); // 2.5KB on Core 0
|
||||
xTaskCreatePinnedToCore(ClientsManager::processMessageQueue, "SendNotifications", 4096, NULL, 1, NULL, 0); // 4KB on Core 0
|
||||
ESP_LOGD(TAG, "SendNotifications task created.");
|
||||
|
||||
// Create time synchronization task (updates deviceTime every second)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "CC1101_Worker.h"
|
||||
#include <sstream> // Moved here from CC1101_Worker.h — only used in this .cpp
|
||||
#include <LittleFS.h> // For pathType 4 (internal flash) transmit support
|
||||
#include "FlipperSubFile.h"
|
||||
#include "modules/subghz_function/StreamingSubFileParser.h"
|
||||
#include "StreamingPulsePayload.h"
|
||||
@@ -904,29 +905,44 @@ bool CC1101Worker::transmitRaw(int module, float frequency, int modulation, floa
|
||||
std::string CC1101Worker::transmitSub(const std::string& filename, int module, int repeat, int pathType)
|
||||
{
|
||||
std::string fullPath;
|
||||
// Determine filesystem: pathType 4 = LittleFS, all others = SD
|
||||
bool useLittleFS = (pathType == 4);
|
||||
fs::FS& fs = useLittleFS ? (fs::FS&)LittleFS : (fs::FS&)SD;
|
||||
|
||||
// If path is already absolute (/DATA/...), use it directly
|
||||
if (filename.find("/DATA/") == 0) {
|
||||
fullPath = filename;
|
||||
ESP_LOGD(TAG, "Using full system path: %s", fullPath.c_str());
|
||||
} else {
|
||||
// Use pathType to determine subdirectory
|
||||
// 0=RECORDS, 1=SIGNALS, 2=PRESETS, 3=TEMP, 4=INTERNAL(LittleFS root), 5=SD root
|
||||
static const char* DIRS[] = {"/DATA/RECORDS", "/DATA/SIGNALS", "/DATA/PRESETS", "/DATA/TEMP"};
|
||||
if (pathType >= 0 && pathType < 4) {
|
||||
fullPath = std::string(DIRS[pathType]) + "/" + filename;
|
||||
ESP_LOGD(TAG, "Using pathType %d: %s", pathType, DIRS[pathType]);
|
||||
} else if (pathType == 4 || pathType == 5) {
|
||||
// Root-based storage: LittleFS root (4) or SD root (5)
|
||||
// filename is relative to root, ensure it starts with "/"
|
||||
if (!filename.empty() && filename[0] == '/') {
|
||||
fullPath = filename;
|
||||
} else {
|
||||
fullPath = "/" + filename;
|
||||
}
|
||||
ESP_LOGI(TAG, "Using pathType %d (%s root): %s",
|
||||
pathType, useLittleFS ? "LittleFS" : "SD", fullPath.c_str());
|
||||
} else {
|
||||
fullPath = std::string("/DATA/RECORDS/") + filename;
|
||||
ESP_LOGW(TAG, "Unknown pathType %d; default RECORDS", pathType);
|
||||
}
|
||||
ESP_LOGD(TAG, "Added base path, full path: %s", fullPath.c_str());
|
||||
}
|
||||
ESP_LOGI(TAG, "Opening file: %s", fullPath.c_str());
|
||||
if (!SD.exists(fullPath.c_str())) {
|
||||
ESP_LOGI(TAG, "Opening file: %s (fs=%s)", fullPath.c_str(), useLittleFS ? "LittleFS" : "SD");
|
||||
if (!fs.exists(fullPath.c_str())) {
|
||||
std::string msg = "File does not exist: " + fullPath;
|
||||
ESP_LOGE(TAG, "%s", msg.c_str());
|
||||
return msg;
|
||||
}
|
||||
File file = SD.open(fullPath.c_str(), FILE_READ);
|
||||
File file = fs.open(fullPath.c_str(), FILE_READ);
|
||||
if (!file) {
|
||||
std::string msg = "Failed to open file: " + fullPath;
|
||||
ESP_LOGE(TAG, "%s", msg.c_str());
|
||||
@@ -934,6 +950,13 @@ std::string CC1101Worker::transmitSub(const std::string& filename, int module, i
|
||||
}
|
||||
ESP_LOGD(TAG, "File opened successfully, size: %d bytes", file.size());
|
||||
file.close(); // Close immediately - will reopen for streaming
|
||||
|
||||
// Transmission from LittleFS is not supported yet (parsers use SD internally)
|
||||
if (useLittleFS) {
|
||||
std::string msg = "Transmission from internal flash (LittleFS) not supported";
|
||||
ESP_LOGE(TAG, "%s", msg.c_str());
|
||||
return msg;
|
||||
}
|
||||
|
||||
// OPTIMIZED: Use StreamingSubFileParser (minimal RAM usage!)
|
||||
StreamingSubFileParser streamParser;
|
||||
|
||||
Reference in New Issue
Block a user