Files
Senape3000 390f49be40 Add bruter module selection and SubGHz DB cloning
Add end-to-end support for selecting which CC1101 module the bruter uses and a new Flipper SubGHz DB cloning workflow.

Firmware:
- Add sub-command (0xF8) to set bruter module from BruterCommands.
- Implement BruterModule::setModule and updatePinsForModule; use selected module in setup and frequency operations.
- Fix CC1101 driver setModul to update currentModule before configuring GDO pins to avoid misconfiguration.

Mobile app:
- Settings UI: add TX Module selector, persist bruterModule, and call BLE command when changed.
- Add FirmwareBinaryProtocol.createBruterSetModuleCommand and BleProvider.setBruterModule API.
- Add BleProvider helpers: uploadFileFromBytes and createDirectory to support file uploads to device storage.
- New FlipperSubDbService: download/extract Zero-Sploit FlipperZero SubGHz repo ZIP and return .sub files.
- Add a Settings workflow/UI (and dialog) to clone the SubGHz DB to the device by creating folders and uploading files via BLE.
- File/Update behavior tweaks: fallback to public Downloads for file saves and APK updates.
- Add archive dependency and related UI/service plumbing.
- Minor UI/branding tweaks (Android package rename, Windows product metadata) and small widget cleanup.

Overall: implements module selection end-to-end (firmware + app), plus a new feature to download and push Flipper .sub files to the device over BLE.
2026-02-14 15:55:54 +01:00

502 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import '../services/cc1101/cc1101_calculator.dart';
import '../theme/app_colors.dart';
/// Compact widget for displaying CC1101 module status
class ModuleStatusWidget extends StatelessWidget {
final List<Map<String, dynamic>> cc1101Modules;
final Map<String, dynamic>? deviceInfo;
// nRF24 status
final bool nrfPresent;
final bool nrfInitialized;
final bool nrfJammerRunning;
final bool nrfScanning;
final bool nrfAttacking;
final bool nrfSpectrumRunning;
// SD card status
final bool sdMounted;
final int sdTotalMB;
final int sdFreeMB;
const ModuleStatusWidget({
super.key,
required this.cc1101Modules,
this.deviceInfo,
this.nrfPresent = false,
this.nrfInitialized = false,
this.nrfJammerRunning = false,
this.nrfScanning = false,
this.nrfAttacking = false,
this.nrfSpectrumRunning = false,
this.sdMounted = false,
this.sdTotalMB = 0,
this.sdFreeMB = 0,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with device info
_buildHeader(context),
const SizedBox(height: 12),
// Module info
...cc1101Modules.asMap().entries.map((entry) {
final index = entry.key;
final module = entry.value;
return _buildModuleCard(context, index, module);
}),
// nRF24 module status
_buildNrfCard(context),
// SD card status
_buildSdCard(context),
],
),
),
);
}
Widget _buildHeader(BuildContext context) {
final freeHeap = deviceInfo?['freeHeap'] ?? 0;
return Row(
children: [
Icon(
Icons.memory,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!.deviceStatus,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppColors.primaryText,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${(freeHeap / 1024).toStringAsFixed(1)} KB',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
],
);
}
Widget _buildModuleCard(BuildContext context, int index, Map<String, dynamic> module) {
final moduleId = module['id'] ?? index;
final mode = module['mode'] ?? 'Unknown';
final settings = module['settings'] ?? '';
// Parse module settings
CC1101Config? config;
try {
if (settings.isNotEmpty) {
config = parseSettingsFromString(settings);
}
} catch (e) {
// If parsing failed, show error
}
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Module header
Row(
children: [
Icon(
Icons.settings_input_antenna,
size: 18,
color: _getModeColor(context, mode),
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!.subGhzModule(moduleId + 1),
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.primaryText,
),
),
const Spacer(),
_buildModeChip(context, mode),
],
),
if (config != null) ...[
const SizedBox(height: 8),
_buildConfigInfo(context, config),
] else if (settings.isNotEmpty) ...[
const SizedBox(height: 8),
_buildSettingsError(context, settings),
],
],
),
);
}
Widget _buildModeChip(BuildContext context, String mode) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _getModeColor(context, mode).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getLocalizedMode(context, mode),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: _getModeColor(context, mode),
),
),
);
}
Widget _buildConfigInfo(BuildContext context, CC1101Config config) {
return Column(
children: [
// Frequency and modulation
Row(
children: [
Expanded(
child: _buildInfoItem(
context,
Icons.graphic_eq,
'${config.frequency.toStringAsFixed(1)} ${AppLocalizations.of(context)!.mhz}',
AppLocalizations.of(context)!.frequency,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildInfoItem(
context,
Icons.radio,
_getLocalizedModulationName(context, config.modulationName),
AppLocalizations.of(context)!.modulation,
),
),
],
),
const SizedBox(height: 6),
// Data Rate and Bandwidth
Row(
children: [
Expanded(
child: _buildInfoItem(
context,
Icons.speed,
'${(config.dataRate / 1000).toStringAsFixed(1)} ${AppLocalizations.of(context)!.kbps}',
AppLocalizations.of(context)!.dataRate,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildInfoItem(
context,
Icons.straighten,
'${(config.bandwidth / 1000).toStringAsFixed(1)} ${AppLocalizations.of(context)!.khz}',
AppLocalizations.of(context)!.bandwidth,
),
),
],
),
// Deviation for FSK modulations (2-FSK or GFSK)
if (config.modulation == 0 || config.modulation == 1) ...[
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: _buildInfoItem(
context,
Icons.tune,
'${(config.deviation / 1000).toStringAsFixed(2)} ${AppLocalizations.of(context)!.khz}',
AppLocalizations.of(context)!.deviation,
),
),
const Spacer(),
],
),
],
],
);
}
Widget _buildInfoItem(BuildContext context, IconData icon, String value, String label) {
return Row(
children: [
Icon(
icon,
size: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 10,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
Widget _buildSettingsError(BuildContext context, String settings) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Icon(
Icons.error_outline,
size: 16,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.settingsParseError,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.error,
),
),
Text(
'Raw: ${settings.substring(0, 50)}${settings.length > 50 ? '...' : ''}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
);
}
String _getLocalizedMode(BuildContext context, String mode) {
final l10n = AppLocalizations.of(context)!;
switch (mode.toLowerCase()) {
case 'idle':
return l10n.statusIdle;
case 'record':
case 'recording':
case 'recordsignal':
return l10n.statusRecording;
case 'transmit':
case 'transmitting':
return l10n.statusTransmitting;
case 'scan':
case 'scanning':
case 'detectsignal':
return l10n.statusScanning;
default:
return mode;
}
}
String _getLocalizedModulationName(BuildContext context, String modulationName) {
final l10n = AppLocalizations.of(context)!;
switch (modulationName) {
case 'ASK/OOK':
return l10n.modulationAskOok;
case '2-FSK':
return l10n.modulation2Fsk;
case '4-FSK':
return l10n.modulation4Fsk;
case 'GFSK':
return l10n.modulationGfsk;
case 'MSK':
return l10n.modulationMsk;
default:
return modulationName;
}
}
Color _getModeColor(BuildContext context, String mode) {
return AppColors.getModuleStatusColor(mode);
}
// ── nRF24 module status card ──────────────────────────────────
Widget _buildNrfCard(BuildContext context) {
// Determine nRF state string and color
String stateStr;
Color stateColor;
if (!nrfPresent) {
stateStr = 'Not Present';
stateColor = AppColors.disabledText;
} else if (!nrfInitialized) {
stateStr = 'Not Initialized';
stateColor = AppColors.warning;
} else if (nrfJammerRunning) {
stateStr = 'Jamming';
stateColor = AppColors.error;
} else if (nrfScanning) {
stateStr = 'Scanning';
stateColor = AppColors.info;
} else if (nrfAttacking) {
stateStr = 'Attacking';
stateColor = const Color(0xFFFF9100);
} else if (nrfSpectrumRunning) {
stateStr = 'Spectrum';
stateColor = AppColors.info;
} else {
stateStr = 'Idle';
stateColor = AppColors.success;
}
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.router,
size: 18,
color: stateColor,
),
const SizedBox(width: 8),
Text(
'nRF24L01+',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.primaryText,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: stateColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
stateStr,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: stateColor,
),
),
),
],
),
);
}
// ── SD card status card ───────────────────────────────────────
Widget _buildSdCard(BuildContext context) {
final Color iconColor;
final String statusText;
if (!sdMounted) {
iconColor = AppColors.disabledText;
statusText = 'Not Inserted';
} else {
iconColor = AppColors.success;
statusText = '${sdFreeMB} MB free / ${sdTotalMB} MB';
}
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.sd_card,
size: 18,
color: iconColor,
),
const SizedBox(width: 8),
Text(
'SD Card',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.primaryText,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
statusText,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: iconColor,
),
),
),
],
),
);
}
}